diff --git a/bridge/bridge.py b/bridge/bridge.py
index 1813f6b..4565b4e 100644
--- a/bridge/bridge.py
+++ b/bridge/bridge.py
@@ -64,6 +64,8 @@ def parse_trigger(body):
if s == f"{TRIGGER} --quick":
return True, True
return False, False
+
+
ALLOWLIST = {u.strip() for u in os.environ.get("AUTH_ALLOWLIST", "").split(",") if u.strip()}
@@ -167,8 +169,12 @@ def post_commit_status(owner, repo, sha, state, target_url, description=""):
f"{GITEA_API}/repos/{owner}/{repo}/statuses/{sha}",
GITEA_TOKEN,
method="POST",
- data={"state": state, "target_url": target_url,
- "description": description, "context": "cc-ci/testme"},
+ data={
+ "state": state,
+ "target_url": target_url,
+ "description": description,
+ "context": "cc-ci/testme",
+ },
)
@@ -217,7 +223,9 @@ def result_comment_body(recipe, sha, num, run_url, status):
if artifact_available(badge_url):
body += f"\n\n[]({run_url})"
return f"{body}\n\n{links}"
- return f"{header} → {run_url}\n\n_(summary card unavailable — see the run for details.)_ {links}"
+ return (
+ f"{header} → {run_url}\n\n_(summary card unavailable — see the run for details.)_ {links}"
+ )
def watch_and_reflect(owner, name, number, num, recipe, sha, comment_id, run_url):
diff --git a/dashboard/dashboard.py b/dashboard/dashboard.py
index 831f254..48de3d3 100644
--- a/dashboard/dashboard.py
+++ b/dashboard/dashboard.py
@@ -66,8 +66,13 @@ _COLORS = {
# Level → colour ramp, kept in sync with runner/harness/card.py LEVEL_COLOR (the dashboard is a
# standalone stdlib service that doesn't import the runner harness, so the small map is duplicated).
_LEVEL_COLOR = {
- 0: "#e5534b", 1: "#e0823d", 2: "#e0823d", 3: "#d9b343",
- 4: "#a0b93f", 5: "#57ab5a", 6: "#3fb950",
+ 0: "#e5534b",
+ 1: "#e0823d",
+ 2: "#e0823d",
+ 3: "#d9b343",
+ 4: "#a0b93f",
+ 5: "#57ab5a",
+ 6: "#3fb950",
}
@@ -269,7 +274,11 @@ def _card(r):
f''
f'no screenshot{_level_pill(r["level"])}'
)
- cap = f'
{html.escape(r["level_cap_reason"])}
' if r["level_cap_reason"] else ""
+ cap = (
+ f'{html.escape(r["level_cap_reason"])}
'
+ if r["level_cap_reason"]
+ else ""
+ )
return (
f'{shot}
'
f'
{html.escape(r["recipe"])}
'
@@ -307,7 +316,11 @@ def render_history(recipe, rows):
trs = []
for r in rows:
color = _COLORS.get(r["status"], "#8b949e")
- lvl = "—" if r["level"] is None else f'
L{int(r["level"])}'
+ lvl = (
+ "—"
+ if r["level"] is None
+ else f'
L{int(r["level"])}'
+ )
shot = f'
card' if r["has_screenshot"] else "—"
trs.append(
f'
| #{r["number"]} | '
@@ -317,7 +330,7 @@ def render_history(recipe, rows):
)
body = "\n".join(trs) or '
| no runs for this recipe yet |
'
inner = (
- f'
{_FLOWER} {html.escape(recipe)} — run history
'
+ f"
{_FLOWER} {html.escape(recipe)} — run history
"
'
← all recipes · every !testme run, newest first.
'
"
| Run | Status | Level | Version | "
"When | Card |
"
diff --git a/flake.nix b/flake.nix
index a1d9f58..48ffb7a 100644
--- a/flake.nix
+++ b/flake.nix
@@ -31,34 +31,36 @@
];
in
{
- # Canonical live host target: the Hetzner cc-ci server.
- # Use `.#cc-ci` for the current production host.
- nixosConfigurations.cc-ci = nixpkgs.lib.nixosSystem {
- inherit system;
- modules = [
- sops-nix.nixosModules.sops
- ./nix/hosts/cc-ci-hetzner/configuration.nix
- ];
- };
+ nixosConfigurations = {
+ # Canonical live host target: the Hetzner cc-ci server.
+ # Use `.#cc-ci` for the current production host.
+ cc-ci = nixpkgs.lib.nixosSystem {
+ inherit system;
+ modules = [
+ sops-nix.nixosModules.sops
+ ./nix/hosts/cc-ci-hetzner/configuration.nix
+ ];
+ };
- # Legacy Incus VM host definition retained only for historical comparison and fallback.
- # Do NOT use this target on the live Hetzner server.
- nixosConfigurations.cc-ci-incus = nixpkgs.lib.nixosSystem {
- inherit system;
- modules = [
- sops-nix.nixosModules.sops
- ./nix/hosts/cc-ci/configuration.nix
- ];
- };
+ # Legacy Incus VM host definition retained only for historical comparison and fallback.
+ # Do NOT use this target on the live Hetzner server.
+ cc-ci-incus = nixpkgs.lib.nixosSystem {
+ inherit system;
+ modules = [
+ sops-nix.nixosModules.sops
+ ./nix/hosts/cc-ci/configuration.nix
+ ];
+ };
- # Explicit alias for the live Hetzner host. Kept alongside `cc-ci` so the intended host target
- # remains obvious in recovery/migration workflows.
- nixosConfigurations.cc-ci-hetzner = nixpkgs.lib.nixosSystem {
- inherit system;
- modules = [
- sops-nix.nixosModules.sops
- ./nix/hosts/cc-ci-hetzner/configuration.nix
- ];
+ # Explicit alias for the live Hetzner host. Kept alongside `cc-ci` so the intended host
+ # target remains obvious in recovery/migration workflows.
+ cc-ci-hetzner = nixpkgs.lib.nixosSystem {
+ inherit system;
+ modules = [
+ sops-nix.nixosModules.sops
+ ./nix/hosts/cc-ci-hetzner/configuration.nix
+ ];
+ };
};
devShells.${system} = {
diff --git a/nix/hosts/cc-ci-hetzner/configuration.nix b/nix/hosts/cc-ci-hetzner/configuration.nix
index 388f293..243b86a 100644
--- a/nix/hosts/cc-ci-hetzner/configuration.nix
+++ b/nix/hosts/cc-ci-hetzner/configuration.nix
@@ -7,7 +7,7 @@
# git clone --recursive https://git.autonomic.zone/recipe-maintainers/cc-ci.git /etc/cc-ci
# install -m600 /var/lib/sops-nix/key.txt
# nixos-rebuild switch --flake /etc/cc-ci#cc-ci-hetzner
-{ pkgs, lib, ... }:
+{ pkgs, ... }:
{
imports = [
./hardware.nix
diff --git a/nix/hosts/cc-ci-hetzner/hardware.nix b/nix/hosts/cc-ci-hetzner/hardware.nix
index d19d0ac..dec7e73 100644
--- a/nix/hosts/cc-ci-hetzner/hardware.nix
+++ b/nix/hosts/cc-ci-hetzner/hardware.nix
@@ -11,13 +11,17 @@
{
imports = [ (modulesPath + "/profiles/qemu-guest.nix") ];
- boot.loader = {
- efi.efiSysMountPoint = "/boot/efi";
- grub = {
- efiSupport = true;
- efiInstallAsRemovable = true;
- device = "nodev";
+ boot = {
+ loader = {
+ efi.efiSysMountPoint = "/boot/efi";
+ grub = {
+ efiSupport = true;
+ efiInstallAsRemovable = true;
+ device = "nodev";
+ };
};
+ initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "xen_blkfront" "vmw_pvscsi" ];
+ initrd.kernelModules = [ "nvme" ];
};
fileSystems."/boot/efi" = {
@@ -25,9 +29,6 @@
fsType = "vfat";
};
- boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "xen_blkfront" "vmw_pvscsi" ];
- boot.initrd.kernelModules = [ "nvme" ];
-
fileSystems."/" = {
device = "/dev/sda1";
fsType = "ext4";
diff --git a/nix/modules/nightly-sweep.nix b/nix/modules/nightly-sweep.nix
index 42018d3..c31b70a 100644
--- a/nix/modules/nightly-sweep.nix
+++ b/nix/modules/nightly-sweep.nix
@@ -29,7 +29,7 @@ in
serviceConfig = {
Type = "oneshot";
# A full sweep across several recipes (each a cold deploy/test/teardown) is long; bound it.
- TimeoutStartSec = "21600"; # 6h ceiling
+ TimeoutStartSec = "21600"; # 6h ceiling
ExecStart = "${sweep}/bin/cc-ci-nightly-sweep";
};
};
@@ -39,7 +39,7 @@ in
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "*-*-* 03:00:00";
- Persistent = true; # catch up a missed nightly after downtime
+ Persistent = true; # catch up a missed nightly after downtime
RandomizedDelaySec = "600";
};
};
diff --git a/runner/harness/abra.py b/runner/harness/abra.py
index a17d24f..1c92066 100644
--- a/runner/harness/abra.py
+++ b/runner/harness/abra.py
@@ -168,7 +168,9 @@ def secret_generate(domain: str, timeout: int = 300) -> None:
)
-def deploy(domain: str, chaos: bool = True, timeout: int = 900, no_converge_checks: bool = False) -> None:
+def deploy(
+ domain: str, chaos: bool = True, timeout: int = 900, no_converge_checks: bool = False
+) -> None:
args = ["app", "deploy", domain, "-o", "-n"]
if chaos:
args.append("-C")
@@ -203,7 +205,10 @@ def backup_create(domain: str, timeout: int = 900) -> str:
# remote and fails "authentication required: Unauthorized". Returns the captured output, whose
# restic JSON summary line carries the produced "snapshot_id" (the backup artifact, DG3) — note
# `abra app backup snapshots` needs a TTY and is awkward to script, so we read the create output.
- out = _run_pty(["app", "backup", "create", domain, "-n", "-C", "-o"], timeout=timeout).stdout or ""
+ out = (
+ _run_pty(["app", "backup", "create", domain, "-n", "-C", "-o"], timeout=timeout).stdout
+ or ""
+ )
# Echo the backup output (incl. backupbot's pre-hook run / any "Failed to run command" or
# "Container ... not running" ERROR) into the run log. Backup is otherwise opaque: a pre-hook that
# fails to register/run leaves the DB dump out of the snapshot, surfacing only as a downstream
diff --git a/runner/harness/browser.py b/runner/harness/browser.py
index 8333a2b..786a7fa 100644
--- a/runner/harness/browser.py
+++ b/runner/harness/browser.py
@@ -13,8 +13,15 @@ from __future__ import annotations
import time
-def goto_with_retry(page, url, *, deadline_seconds: int = 120, accept_statuses=(200, 304),
- goto_timeout_ms: int = 30_000, wait_until: str = "domcontentloaded"):
+def goto_with_retry(
+ page,
+ url,
+ *,
+ deadline_seconds: int = 120,
+ accept_statuses=(200, 304),
+ goto_timeout_ms: int = 30_000,
+ wait_until: str = "domcontentloaded",
+):
"""Poll `page.goto(url)` until status is in `accept_statuses` OR the deadline expires.
Returns the final Playwright response. Raises AssertionError if the deadline expires without
diff --git a/runner/harness/canonical.py b/runner/harness/canonical.py
index 64e21b2..a80d299 100644
--- a/runner/harness/canonical.py
+++ b/runner/harness/canonical.py
@@ -55,7 +55,9 @@ def enrolled_recipes() -> list[str]:
out = []
try:
for name in sorted(os.listdir(tests_dir)):
- if os.path.isfile(os.path.join(tests_dir, name, "recipe_meta.py")) and is_enrolled(name):
+ if os.path.isfile(os.path.join(tests_dir, name, "recipe_meta.py")) and is_enrolled(
+ name
+ ):
out.append(name)
except OSError:
pass
@@ -122,11 +124,15 @@ def deploy_canonical(recipe: str, timeout: int = 900) -> None:
abra.recipe_checkout(recipe, version)
r = subprocess.run(
["abra", "app", "deploy", domain, version, "-o", "-n", "-f"],
- capture_output=True, text=True, timeout=timeout,
+ capture_output=True,
+ text=True,
+ timeout=timeout,
)
if r.returncode != 0:
- raise RuntimeError(f"deploy canonical {domain} {version} failed: "
- f"{(r.stderr + ' ' + r.stdout).strip()[:300]}")
+ raise RuntimeError(
+ f"deploy canonical {domain} {version} failed: "
+ f"{(r.stderr + ' ' + r.stdout).strip()[:300]}"
+ )
_set_status(recipe, "warm")
diff --git a/runner/harness/card.py b/runner/harness/card.py
index 6f44d2a..28be566 100644
--- a/runner/harness/card.py
+++ b/runner/harness/card.py
@@ -148,7 +148,9 @@ RUNG_LABEL = {
"backup_restore": "backup/restore",
"functional": "functional",
}
-SKIP_GREEN = "#57ab5a" # muted green — an intentional skip reads like a pass (but labelled, never inflating)
+SKIP_GREEN = (
+ "#57ab5a" # muted green — an intentional skip reads like a pass (but labelled, never inflating)
+)
def _skip_rows(skips: dict) -> str:
@@ -159,14 +161,16 @@ def _skip_rows(skips: dict) -> str:
for rung, reason in (skips.get("intentional") or {}).items():
rows.append(
f'| ⊘'
- f'{html.escape(RUNG_LABEL.get(rung, rung))} | '
+ f"{html.escape(RUNG_LABEL.get(rung, rung))}"
f'intentional skip |
'
)
- rows.append(f' | {html.escape(reason)} |
')
+ rows.append(
+ f' | {html.escape(reason)} |
'
+ )
for rung in skips.get("unintentional") or []:
rows.append(
f'| ⊘'
- f'{html.escape(RUNG_LABEL.get(rung, rung))} | '
+ f"{html.escape(RUNG_LABEL.get(rung, rung))}"
f'unintentional skip |
'
)
rows.append(
diff --git a/runner/harness/deps.py b/runner/harness/deps.py
index ba131cc..bd679a1 100644
--- a/runner/harness/deps.py
+++ b/runner/harness/deps.py
@@ -28,7 +28,7 @@ from __future__ import annotations
import contextlib
import json
import os
-from typing import Iterable
+from collections.abc import Iterable
from . import lifecycle, naming
@@ -36,9 +36,7 @@ from . import lifecycle, naming
def declared_deps(recipe: str) -> list[str]:
"""Read `DEPS` from `tests//recipe_meta.py` — a list of recipe names this recipe needs
deployed alongside it. Returns [] if none."""
- path = os.path.join(
- os.path.dirname(__file__), "..", "..", "tests", recipe, "recipe_meta.py"
- )
+ path = os.path.join(os.path.dirname(__file__), "..", "..", "tests", recipe, "recipe_meta.py")
if not os.path.exists(path):
return []
ns: dict = {}
diff --git a/runner/harness/generic.py b/runner/harness/generic.py
index 216b1ae..f42b896 100644
--- a/runner/harness/generic.py
+++ b/runner/harness/generic.py
@@ -222,7 +222,11 @@ def assert_restore_healthy(domain: str, meta: dict) -> None:
def perform_upgrade(
- domain: str, recipe: str, head_ref: str | None, deploy_timeout: int = 900, meta: dict | None = None
+ domain: str,
+ recipe: str,
+ head_ref: str | None,
+ deploy_timeout: int = 900,
+ meta: dict | None = None,
) -> dict[str, str | None]:
"""Perform the UPGRADE op once, in place, to the PR-HEAD code under test (HC1): re-checkout the
PR head (the prev-tag base deploy reset the recipe working tree), then `abra app deploy --chaos`
@@ -267,7 +271,9 @@ def perform_upgrade(
deploy_timeout=int(meta.get("DEPLOY_TIMEOUT", deploy_timeout)),
http_timeout=int(meta.get("HTTP_TIMEOUT", 300)),
)
- lifecycle.wait_ready_probes(meta, domain, timeout=int(meta.get("DEPLOY_TIMEOUT", deploy_timeout)))
+ lifecycle.wait_ready_probes(
+ meta, domain, timeout=int(meta.get("DEPLOY_TIMEOUT", deploy_timeout))
+ )
after = lifecycle.deployed_identity(domain)
# Evidence (HC1): the chaos-version label = the deployed recipe commit; it should match the
# PR-head we checked out — proving the upgrade deployed the code under test, not a published tag.
diff --git a/runner/harness/http.py b/runner/harness/http.py
index 002d9b6..1f1e802 100644
--- a/runner/harness/http.py
+++ b/runner/harness/http.py
@@ -73,7 +73,7 @@ def http_post(
`data` is JSON-encoded if content_type='application/json',
form-encoded if 'application/x-www-form-urlencoded' (the OIDC token endpoint form),
or sent raw bytes if data is already bytes."""
- if isinstance(data, (bytes, bytearray)):
+ if isinstance(data, bytes | bytearray):
body: bytes | None = bytes(data)
elif content_type == "application/json" and data is not None:
body = json.dumps(data).encode()
@@ -107,7 +107,7 @@ def http_request(
) -> tuple[int, object | None]:
"""Arbitrary-method HTTP (PUT/DELETE/PATCH) for parity tests that mutate. Same shape as
http_post (returns (status, json_or_None))."""
- if isinstance(data, (bytes, bytearray)):
+ if isinstance(data, bytes | bytearray):
body: bytes | None = bytes(data)
elif content_type == "application/json" and data is not None:
body = json.dumps(data).encode()
@@ -142,7 +142,7 @@ def post_with_headers(
"""Like http_post but ALSO returns the response headers as a dict — for APIs that hand back an
auth token in a response header rather than the body (e.g. mattermost login → `Token` header).
Returns (status, parsed_json_or_None, response_headers). status=0 + {} on transport failure."""
- if isinstance(data, (bytes, bytearray)):
+ if isinstance(data, bytes | bytearray):
body: bytes | None = bytes(data)
elif content_type == "application/json" and data is not None:
body = json.dumps(data).encode()
@@ -252,13 +252,16 @@ def retry_http_post(
) -> tuple[int, object | None]:
"""POST with retry until expect_fn(status, json) is truthy. Defaults to any 2xx."""
if expect_fn is None:
+
def expect_fn(s, _j): # noqa: ARG001
return 200 <= s < 300
result: list[tuple[int, object | None]] = [(0, None)]
def _check():
- s, j = http_post(url, data=data, headers=headers, content_type=content_type, timeout=timeout)
+ s, j = http_post(
+ url, data=data, headers=headers, content_type=content_type, timeout=timeout
+ )
result[0] = (s, j)
return expect_fn(s, j)
diff --git a/runner/harness/warmsnap.py b/runner/harness/warmsnap.py
index 36170c6..4c97863 100644
--- a/runner/harness/warmsnap.py
+++ b/runner/harness/warmsnap.py
@@ -113,7 +113,9 @@ def _assert_undeployed(domain: str) -> None:
)
-def snapshot(recipe: str, domain: str, commit: str | None = None, version: str | None = None) -> dict:
+def snapshot(
+ recipe: str, domain: str, commit: str | None = None, version: str | None = None
+) -> dict:
"""Take a last-known-good snapshot of every data volume of 's stack. The app MUST be
undeployed. Atomically replaces the prior last-good. Returns the written meta dict."""
_assert_undeployed(domain)
@@ -169,7 +171,9 @@ def restore(recipe: str, domain: str) -> dict:
for vol in meta.get("volumes", []):
tar_path = os.path.join(volumes_dir(recipe), f"{vol}.tar")
if vol not in current:
- raise SnapshotError(f"snapshot volume {vol} absent from current stack {sorted(current)}")
+ raise SnapshotError(
+ f"snapshot volume {vol} absent from current stack {sorted(current)}"
+ )
mp = _volume_mountpoint(vol)
# Clear the volume contents (incl. dotfiles) without removing the mountpoint itself.
r = _run(["sh", "-c", f'rm -rf -- "{mp}"/* "{mp}"/.[!.]* "{mp}"/..?* 2>/dev/null; true'])
diff --git a/runner/nightly_sweep.py b/runner/nightly_sweep.py
index cf233c5..121d0ca 100644
--- a/runner/nightly_sweep.py
+++ b/runner/nightly_sweep.py
@@ -60,14 +60,17 @@ def sweep() -> int:
for r in recipes:
print(f"\n===== nightly: full-cold {r} (latest) =====", flush=True)
env = dict(os.environ, RECIPE=r)
- env.pop("REF", None) # latest, not a PR head
+ env.pop("REF", None) # latest, not a PR head
env.pop("CCCI_QUICK", None)
env.pop("MODE", None)
rc = subprocess.run(
[sys.executable, os.path.join(_here(), "run_recipe_ci.py")], env=env
).returncode
results[r] = rc
- print(f"nightly: {r} rc={rc} ({'green→canonical refreshed' if rc == 0 else 'red'})", flush=True)
+ print(
+ f"nightly: {r} rc={rc} ({'green→canonical refreshed' if rc == 0 else 'red'})",
+ flush=True,
+ )
# WC8 disk hygiene: drop warm data for de-enrolled canonicals; log the disk budget.
pruned = canonical.prune_stale()
if pruned:
diff --git a/runner/warm_reconcile.py b/runner/warm_reconcile.py
index f3e8496..d9e58b7 100644
--- a/runner/warm_reconcile.py
+++ b/runner/warm_reconcile.py
@@ -43,11 +43,16 @@ def _traefik_setup(recipe: str, domain: str, version: str) -> None:
ssl_cert/ssl_key swarm secrets; NO ACME). Uses the proven abra.env_set (newline-safe, unlike the
bash set_env that bit keycloak)."""
cert_dir = "/var/lib/ci-certs/live"
- if not (os.path.isfile(f"{cert_dir}/fullchain.pem") and os.path.isfile(f"{cert_dir}/privkey.pem")):
+ if not (
+ os.path.isfile(f"{cert_dir}/fullchain.pem") and os.path.isfile(f"{cert_dir}/privkey.pem")
+ ):
raise RuntimeError(f"FATAL: wildcard cert missing at {cert_dir} (sops decrypt broken?)")
if not os.path.isfile(env_file(domain)):
- _run(["abra", "app", "new", recipe, "-s", "default", "-D", domain, version, "-o", "-n"],
- timeout=120, check=True)
+ _run(
+ ["abra", "app", "new", recipe, "-s", "default", "-D", domain, version, "-o", "-n"],
+ timeout=120,
+ check=True,
+ )
abra.env_set(domain, "DOMAIN", domain)
abra.env_set(domain, "LETS_ENCRYPT_ENV", "")
abra.env_set(domain, "WILDCARDS_ENABLED", "1")
@@ -61,11 +66,39 @@ def _traefik_setup(recipe: str, domain: str, version: str) -> None:
return any(s.endswith(f"_{name}_v1") for s in have)
if not _has("ssl_cert"):
- _run(["abra", "app", "secret", "insert", domain, "ssl_cert", "v1",
- f"{cert_dir}/fullchain.pem", "-f", "-n"], timeout=120, check=True)
+ _run(
+ [
+ "abra",
+ "app",
+ "secret",
+ "insert",
+ domain,
+ "ssl_cert",
+ "v1",
+ f"{cert_dir}/fullchain.pem",
+ "-f",
+ "-n",
+ ],
+ timeout=120,
+ check=True,
+ )
if not _has("ssl_key"):
- _run(["abra", "app", "secret", "insert", domain, "ssl_key", "v1",
- f"{cert_dir}/privkey.pem", "-f", "-n"], timeout=120, check=True)
+ _run(
+ [
+ "abra",
+ "app",
+ "secret",
+ "insert",
+ domain,
+ "ssl_key",
+ "v1",
+ f"{cert_dir}/privkey.pem",
+ "-f",
+ "-n",
+ ],
+ timeout=120,
+ check=True,
+ )
SPECS: dict[str, dict] = {
@@ -218,8 +251,17 @@ def health_code(spec: dict) -> int:
domain = spec.get("health_domain", spec["domain"])
r = _run(
[
- "curl", "-sk", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", "10",
- "--resolve", f"{domain}:443:127.0.0.1", f"https://{domain}{spec['health_path']}",
+ "curl",
+ "-sk",
+ "-o",
+ "/dev/null",
+ "-w",
+ "%{http_code}",
+ "--max-time",
+ "10",
+ "--resolve",
+ f"{domain}:443:127.0.0.1",
+ f"https://{domain}{spec['health_path']}",
],
timeout=20,
)
@@ -230,7 +272,6 @@ def health_code(spec: dict) -> int:
def wait_healthy(spec: dict, timeout: int | None = None) -> bool:
- domain = spec["domain"]
deadline = time.time() + (timeout or spec["health_timeout"])
while time.time() < deadline:
if health_code(spec) in tuple(spec["health_ok"]):
@@ -325,15 +366,18 @@ def ensure_server() -> None:
def ensure_app_config(recipe: str, domain: str, version: str) -> None:
if not os.path.isfile(env_file(domain)):
- _run(["abra", "app", "new", recipe, "-s", "default", "-D", domain, version, "-o", "-n"],
- timeout=120, check=True)
+ _run(
+ ["abra", "app", "new", recipe, "-s", "default", "-D", domain, version, "-o", "-n"],
+ timeout=120,
+ check=True,
+ )
abra.env_set(domain, "DOMAIN", domain)
abra.env_set(domain, "LETS_ENCRYPT_ENV", "")
def ensure_secrets(domain: str) -> None:
stack = lifecycle._stack_name(domain) # noqa: SLF001
- have = {n for n in lifecycle._docker_names("secret", stack)} # noqa: SLF001
+ have = set(lifecycle._docker_names("secret", stack)) # noqa: SLF001
if not any(n.endswith("_admin_password_v1") for n in have):
abra.secret_generate(domain)
@@ -393,8 +437,9 @@ def reconcile(app: str) -> str:
write_alert(app, "held-major", current=current, latest=latest, release_notes=notes[:4000])
return f"held-major:{current}->{latest}"
if notes_flag_manual_migration(notes):
- write_alert(app, "held-manual-migration", current=current, latest=latest,
- release_notes=notes[:4000])
+ write_alert(
+ app, "held-manual-migration", current=current, latest=latest, release_notes=notes[:4000]
+ )
return f"held-manual-migration:{current}->{latest}"
# WC1.1 health-gated upgrade with rollback.
@@ -428,8 +473,14 @@ def reconcile(app: str) -> str:
warmsnap.restore(recipe, domain)
deploy_version(recipe, domain, last_good, dt)
recovered = wait_healthy(spec)
- write_alert(app, "rollback", last_good=last_good, attempted=latest, recovered=recovered,
- release_notes=notes[:2000])
+ write_alert(
+ app,
+ "rollback",
+ last_good=last_good,
+ attempted=latest,
+ recovered=recovered,
+ release_notes=notes[:2000],
+ )
if not recovered:
raise RuntimeError(f"{app} rollback to {last_good} did not become healthy")
return f"rolled-back:{latest}->{last_good}"
diff --git a/tests/bluesky-pds/_p4.py b/tests/bluesky-pds/_p4.py
index 58063e1..29b2c74 100644
--- a/tests/bluesky-pds/_p4.py
+++ b/tests/bluesky-pds/_p4.py
@@ -15,7 +15,8 @@ import shlex
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
-from harness import http as harness_http, lifecycle # noqa: E402
+from harness import http as harness_http # noqa: E402
+from harness import lifecycle
PDS_HOST_LOCAL = "http://localhost:3000"
_PW = "ccci-P4-marker-pw-2026"
diff --git a/tests/bluesky-pds/functional/test_account_and_post.py b/tests/bluesky-pds/functional/test_account_and_post.py
index 5d0cb9a..ea8604f 100644
--- a/tests/bluesky-pds/functional/test_account_and_post.py
+++ b/tests/bluesky-pds/functional/test_account_and_post.py
@@ -27,6 +27,7 @@ CRUD). A wedged PDS subsystem fails AT its layer.
from __future__ import annotations
+import contextlib
import os
import re
import secrets
@@ -35,7 +36,8 @@ import sys
import uuid
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner"))
-from harness import http as harness_http, lifecycle # noqa: E402
+from harness import http as harness_http # noqa: E402
+from harness import lifecycle
PDS_HOST_LOCAL = "http://localhost:3000"
@@ -58,14 +60,18 @@ def _goat_admin(domain: str, args: str) -> str:
return _in_container(domain, cmd)
-def _xrpc_post(domain: str, nsid: str, data: dict, token: str | None = None) -> tuple[int, dict | None]:
+def _xrpc_post(
+ domain: str, nsid: str, data: dict, token: str | None = None
+) -> tuple[int, dict | None]:
headers = {}
if token:
headers["Authorization"] = f"Bearer {token}"
return harness_http.http_post(f"https://{domain}/xrpc/{nsid}", data=data, headers=headers)
-def _xrpc_get(domain: str, nsid: str, query: str, token: str | None = None) -> tuple[int, dict | None]:
+def _xrpc_get(
+ domain: str, nsid: str, query: str, token: str | None = None
+) -> tuple[int, dict | None]:
headers = {}
if token:
headers["Authorization"] = f"Bearer {token}"
@@ -82,9 +88,9 @@ def test_account_lifecycle_and_post_roundtrip(live_app):
# Step 1: PDS describe via goat — recipe self-identifies as did:web:
out = _in_container(domain, f"goat pds describe {PDS_HOST_LOCAL} 2>&1")
- assert f"did:web:{domain}" in out, (
- f"goat pds describe did not contain expected DID 'did:web:{domain}'. Output:\n{out[:500]!r}"
- )
+ assert (
+ f"did:web:{domain}" in out
+ ), f"goat pds describe did not contain expected DID 'did:web:{domain}'. Output:\n{out[:500]!r}"
# Step 2: Create account (UUID-suffixed handle = no run-to-run collision)
out = _goat_admin(
@@ -127,9 +133,9 @@ def test_account_lifecycle_and_post_roundtrip(live_app):
assert s == 200, f"createRecord HTTP {s}: {body!r}"
record_uri = (body or {}).get("uri", "")
# URI format: at:///app.bsky.feed.post/
- assert record_uri.startswith(f"at://{new_did}/app.bsky.feed.post/"), (
- f"unexpected record uri: {record_uri!r}"
- )
+ assert record_uri.startswith(
+ f"at://{new_did}/app.bsky.feed.post/"
+ ), f"unexpected record uri: {record_uri!r}"
rkey = record_uri.rsplit("/", 1)[-1]
assert rkey, f"no rkey in uri: {record_uri!r}"
@@ -142,15 +148,13 @@ def test_account_lifecycle_and_post_roundtrip(live_app):
)
assert s == 200, f"getRecord HTTP {s}: {body!r}"
record_value = (body or {}).get("value", {})
- assert record_value.get("text") == marker, (
- f"post text did not round-trip: created={marker!r}, fetched={record_value.get('text')!r}"
- )
+ assert (
+ record_value.get("text") == marker
+ ), f"post text did not round-trip: created={marker!r}, fetched={record_value.get('text')!r}"
assert record_value.get("$type") == "app.bsky.feed.post"
finally:
# Step 6: Best-effort cleanup. (The per-run domain teardown will discard the volume
# too, but we exercise the delete-account path because it's part of §4.3.)
if cleanup_did:
- try:
+ with contextlib.suppress(Exception):
_goat_admin(domain, f"account delete {cleanup_did}")
- except Exception: # noqa: BLE001
- pass
diff --git a/tests/bluesky-pds/functional/test_describe_server.py b/tests/bluesky-pds/functional/test_describe_server.py
index c993ffe..fd49524 100644
--- a/tests/bluesky-pds/functional/test_describe_server.py
+++ b/tests/bluesky-pds/functional/test_describe_server.py
@@ -26,6 +26,6 @@ def test_describe_server_returns_atproto_envelope(live_app):
# At least one of these atproto-spec fields must be present
expected_any = ("availableUserDomains", "inviteCodeRequired", "links", "did")
present = [k for k in expected_any if k in body]
- assert present, (
- f"describe-server missing all of {expected_any}; got keys: {sorted(body.keys())[:20]}"
- )
+ assert (
+ present
+ ), f"describe-server missing all of {expected_any}; got keys: {sorted(body.keys())[:20]}"
diff --git a/tests/bluesky-pds/functional/test_health_check.py b/tests/bluesky-pds/functional/test_health_check.py
index 0fb19a3..847ab3e 100644
--- a/tests/bluesky-pds/functional/test_health_check.py
+++ b/tests/bluesky-pds/functional/test_health_check.py
@@ -17,6 +17,6 @@ def test_pds_health_returns_version(live_app):
url = f"https://{live_app}/xrpc/_health"
status, body = harness_http.retry_http_get(url, expect_status=200, max_wait=60, interval=3)
assert status == 200, f"GET {url} HTTP {status} (expected 200)"
- assert isinstance(body, dict) and isinstance(body.get("version"), str) and body["version"], (
- f"GET {url} response is not the expected health envelope: {body!r}"
- )
+ assert (
+ isinstance(body, dict) and isinstance(body.get("version"), str) and body["version"]
+ ), f"GET {url} response is not the expected health envelope: {body!r}"
diff --git a/tests/bluesky-pds/functional/test_session_auth.py b/tests/bluesky-pds/functional/test_session_auth.py
index da4c474..f26b417 100644
--- a/tests/bluesky-pds/functional/test_session_auth.py
+++ b/tests/bluesky-pds/functional/test_session_auth.py
@@ -30,6 +30,6 @@ def test_get_session_requires_auth(live_app):
f"body: {body!r}"
)
# The XRPC error envelope is JSON with an `error` field per the atproto spec.
- assert isinstance(body, dict) and body.get("error"), (
- f"expected XRPC JSON error envelope; got: {body!r}"
- )
+ assert isinstance(body, dict) and body.get(
+ "error"
+ ), f"expected XRPC JSON error envelope; got: {body!r}"
diff --git a/tests/bluesky-pds/install_steps.sh b/tests/bluesky-pds/install_steps.sh
index f5ab73e..4a67e00 100755
--- a/tests/bluesky-pds/install_steps.sh
+++ b/tests/bluesky-pds/install_steps.sh
@@ -22,12 +22,12 @@ echo " bluesky-pds install_steps: generating secp256k1 PLC rotation key..."
# same shape the PDS expects (32-byte hex). Equivalent for atproto PDS bootstrap.
KEY_HEX=$(cc-ci-run -c 'import secrets; print(secrets.token_bytes(32).hex())')
if [ -z "${KEY_HEX}" ] || [ "${#KEY_HEX}" != "64" ]; then
- echo " install_steps: failed to generate PLC rotation key (KEY_HEX length=${#KEY_HEX})" >&2
- exit 1
+ echo " install_steps: failed to generate PLC rotation key (KEY_HEX length=${#KEY_HEX})" >&2
+ exit 1
fi
# Insert via abra under TTY-wrap (`abra app secret insert` requires a TTY on this version).
# We DON'T log the key value — abra also doesn't print it.
script -qec "abra app secret insert ${CCCI_APP_DOMAIN} pds_plc_rotation_key v1 ${KEY_HEX} --no-input" /dev/null \
- >/dev/null 2>&1
+ >/dev/null 2>&1
echo " bluesky-pds install_steps: PLC rotation key inserted (v1)."
diff --git a/tests/bluesky-pds/test_restore.py b/tests/bluesky-pds/test_restore.py
index 7bfa04f..6a7e798 100644
--- a/tests/bluesky-pds/test_restore.py
+++ b/tests/bluesky-pds/test_restore.py
@@ -11,6 +11,6 @@ import _p4 # noqa: E402
def test_restore_returns_state(live_app):
- assert _p4.account_exists(live_app), (
- "restore did not bring back the seeded marker account (PDS data did not survive restore)"
- )
+ assert _p4.account_exists(
+ live_app
+ ), "restore did not bring back the seeded marker account (PDS data did not survive restore)"
diff --git a/tests/conftest.py b/tests/conftest.py
index 9575356..e9be5cb 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -13,7 +13,8 @@ import sys
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "runner"))
-from harness import deps as deps_mod, lifecycle, naming # noqa: E402
+from harness import deps as deps_mod # noqa: E402
+from harness import lifecycle, naming
def _short(s: str, n: int = 8) -> str:
diff --git a/tests/cryptpad/playwright/test_pad_content_roundtrip.py b/tests/cryptpad/playwright/test_pad_content_roundtrip.py
index 7218c6b..0b9d1cf 100644
--- a/tests/cryptpad/playwright/test_pad_content_roundtrip.py
+++ b/tests/cryptpad/playwright/test_pad_content_roundtrip.py
@@ -26,6 +26,7 @@ Transient `net::ERR_NETWORK_CHANGED` is handled by the shared `goto_with_retry`
from __future__ import annotations
+import contextlib
import os
import sys
import uuid
@@ -39,7 +40,11 @@ def _open_pad(ctx, url):
bar once CryptPad has created/loaded the fragment-keyed pad (`#/2/pad/edit//`)."""
page = ctx.new_page()
harness_browser.goto_with_retry(
- page, url, accept_statuses=(200,), goto_timeout_ms=60_000, wait_until="load",
+ page,
+ url,
+ accept_statuses=(200,),
+ goto_timeout_ms=60_000,
+ wait_until="load",
deadline_seconds=150,
)
pad_url = url
@@ -53,13 +58,15 @@ def _open_pad(ctx, url):
pad_url = page.url
break
if i == 40:
- try:
+ with contextlib.suppress(Exception): # best-effort unstick
harness_browser.goto_with_retry(
- page, url, accept_statuses=(200,), goto_timeout_ms=60_000,
- wait_until="load", deadline_seconds=120,
+ page,
+ url,
+ accept_statuses=(200,),
+ goto_timeout_ms=60_000,
+ wait_until="load",
+ deadline_seconds=120,
)
- except Exception: # noqa: BLE001 — best-effort unstick
- pass
return page, pad_url
@@ -74,18 +81,22 @@ def _ckeditor_frame(page, deadline_polls=90, reload_at=22, reload_url=None):
if "ckeditor-inner" in f.url:
return f
if i == reload_at and reload_url is not None:
- try:
+ with contextlib.suppress(Exception): # reload is a best-effort unstick
harness_browser.goto_with_retry(
- page, reload_url, accept_statuses=(200,), goto_timeout_ms=60_000,
- wait_until="load", deadline_seconds=120,
+ page,
+ reload_url,
+ accept_statuses=(200,),
+ goto_timeout_ms=60_000,
+ wait_until="load",
+ deadline_seconds=120,
)
- except Exception: # noqa: BLE001 — reload is a best-effort unstick
- pass
page.wait_for_timeout(2000)
return None
-def _poll_any_frame_for_text(page, needle, deadline_polls=120, reload_at=(20, 45, 75, 100), reload_url=None):
+def _poll_any_frame_for_text(
+ page, needle, deadline_polls=120, reload_at=(20, 45, 75, 100), reload_url=None
+):
"""Robust read-back (F2-13): poll EVERY frame's body text for `needle`, returning True as soon as
it appears. The fresh cold-cache read-back context's deeply-nested CKEditor frame is slow/flaky to
*attach* by URL (the prior `_ckeditor_frame` wait timed out on the Adversary's cold run), but the
@@ -101,13 +112,15 @@ def _poll_any_frame_for_text(page, needle, deadline_polls=120, reload_at=(20, 45
except Exception: # noqa: BLE001 — frame not ready / detached; keep polling
pass
if reload_url and i in reload_at:
- try:
+ with contextlib.suppress(Exception): # best-effort unstick
harness_browser.goto_with_retry(
- page, reload_url, accept_statuses=(200,), goto_timeout_ms=60_000,
- wait_until="load", deadline_seconds=120,
+ page,
+ reload_url,
+ accept_statuses=(200,),
+ goto_timeout_ms=60_000,
+ wait_until="load",
+ deadline_seconds=120,
)
- except Exception: # noqa: BLE001 — best-effort unstick
- pass
page.wait_for_timeout(2000)
return False
@@ -137,9 +150,9 @@ def test_cryptpad_pad_content_survives_fresh_session(live_app):
# --- session 1: create the pad + write the marker ---
ctx1 = browser.new_context(ignore_https_errors=True)
page, pad_url = _open_pad(ctx1, f"https://{live_app}/pad/")
- assert "#/2/pad/edit/" in pad_url, (
- f"CryptPad did not create a fragment-keyed pad URL; got {pad_url!r}"
- )
+ assert (
+ "#/2/pad/edit/" in pad_url
+ ), f"CryptPad did not create a fragment-keyed pad URL; got {pad_url!r}"
ck = _ckeditor_frame(page, reload_url=pad_url)
assert ck is not None, "CKEditor content frame never attached (pad editor not ready)"
_dismiss_store_modal(page)
@@ -148,9 +161,9 @@ def test_cryptpad_pad_content_survives_fresh_session(live_app):
page.wait_for_timeout(1000)
body.type(marker, delay=40)
page.wait_for_timeout(12000) # let CryptPad encrypt + sync the update to the server
- assert marker in ck.locator("body").inner_text(), (
- "marker not present in the editor after typing — type did not land"
- )
+ assert (
+ marker in ck.locator("body").inner_text()
+ ), "marker not present in the editor after typing — type did not land"
ctx1.close()
# --- session 2: FRESH context (no shared storage/localStorage) reads the pad back by URL.
diff --git a/tests/cryptpad/playwright/test_pad_create.py b/tests/cryptpad/playwright/test_pad_create.py
index ecabba1..6dd57d0 100644
--- a/tests/cryptpad/playwright/test_pad_create.py
+++ b/tests/cryptpad/playwright/test_pad_create.py
@@ -51,9 +51,9 @@ def test_cryptpad_spa_renders_with_no_console_errors(live_app):
title = (page.title() or "").lower()
body = page.content()
blower = body.lower()
- assert "cryptpad" in title or "cryptpad" in blower, (
- f"CryptPad SPA does not carry brand. title={title!r}, body excerpt: {body[:200]!r}"
- )
+ assert (
+ "cryptpad" in title or "cryptpad" in blower
+ ), f"CryptPad SPA does not carry brand. title={title!r}, body excerpt: {body[:200]!r}"
# Canonical CryptPad asset references in the rendered DOM
canonical = ("/customize/", "/components/", "main.js", "/api/broadcast")
diff --git a/tests/cryptpad/test_install.py b/tests/cryptpad/test_install.py
index 9038d60..ceeff73 100644
--- a/tests/cryptpad/test_install.py
+++ b/tests/cryptpad/test_install.py
@@ -8,7 +8,8 @@ import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
-from harness import browser as harness_browser, generic, lifecycle # noqa: E402
+from harness import browser as harness_browser # noqa: E402
+from harness import generic, lifecycle
def test_serving_and_content(live_app, meta):
diff --git a/tests/custom-html-bkp-bad/test_backup.py b/tests/custom-html-bkp-bad/test_backup.py
index 373fae9..976354c 100644
--- a/tests/custom-html-bkp-bad/test_backup.py
+++ b/tests/custom-html-bkp-bad/test_backup.py
@@ -20,7 +20,9 @@ def test_backup_captures_state(live_app):
Since custom-html-bkp-bad has no ops.py::pre_backup to seed the marker, this file does NOT
exist at backup time — exec_in_app returns empty or raises → assertion fails → backup tier RED.
This models a recipe that declares backup capability but omits the data-seeding hook."""
- result = lifecycle.exec_in_app(live_app, ["sh", "-c", f"cat {MARKER_PATH} 2>/dev/null || echo MISSING"]).strip()
+ result = lifecycle.exec_in_app(
+ live_app, ["sh", "-c", f"cat {MARKER_PATH} 2>/dev/null || echo MISSING"]
+ ).strip()
assert result == "original", (
f"backup did not capture the expected marker at {MARKER_PATH}: got {result!r}. "
"Expected 'original' (seeded by pre_backup). If the marker is 'MISSING', the pre_backup "
diff --git a/tests/custom-html-tiny/functional/test_serves_content.py b/tests/custom-html-tiny/functional/test_serves_content.py
index cb30917..7eb17e6 100644
--- a/tests/custom-html-tiny/functional/test_serves_content.py
+++ b/tests/custom-html-tiny/functional/test_serves_content.py
@@ -79,9 +79,9 @@ def test_static_file_roundtrip_and_404(live_app):
# A random non-existent path must 404 — proves real static-file semantics, distinguishing a
# working server from a 200-everything stub or a mis-routed Traefik fallback.
miss_status, _ = _get(f"https://{live_app}/ccci-missing-{uuid.uuid4().hex}.txt")
- assert miss_status == 404, (
- f"missing path returned {miss_status} (expected 404 — generic 200-returner / mis-route?)"
- )
+ assert (
+ miss_status == 404
+ ), f"missing path returned {miss_status} (expected 404 — generic 200-returner / mis-route?)"
finally:
with contextlib.suppress(OSError):
os.remove(path)
diff --git a/tests/custom-html/functional/test_content_roundtrip.py b/tests/custom-html/functional/test_content_roundtrip.py
index c3a8b5f..7879314 100644
--- a/tests/custom-html/functional/test_content_roundtrip.py
+++ b/tests/custom-html/functional/test_content_roundtrip.py
@@ -15,7 +15,8 @@ import sys
import uuid
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner"))
-from harness import http as harness_http, lifecycle # noqa: E402
+from harness import http as harness_http # noqa: E402
+from harness import lifecycle
def test_content_roundtrip(live_app):
diff --git a/tests/custom-html/functional/test_content_type_header.py b/tests/custom-html/functional/test_content_type_header.py
index 64c2fd9..dba467b 100644
--- a/tests/custom-html/functional/test_content_type_header.py
+++ b/tests/custom-html/functional/test_content_type_header.py
@@ -53,9 +53,9 @@ def test_content_type_html_and_txt(live_app):
ct_txt = h_txt.get("content-type", "")
# nginx default: "text/html" for .html and "text/plain" for .txt (may include "; charset=utf-8")
- assert ct_html.startswith("text/html"), (
- f"{html_name} Content-Type={ct_html!r}, expected text/html (nginx MIME config broken?)"
- )
- assert ct_txt.startswith("text/plain"), (
- f"{txt_name} Content-Type={ct_txt!r}, expected text/plain (nginx MIME config broken?)"
- )
+ assert ct_html.startswith(
+ "text/html"
+ ), f"{html_name} Content-Type={ct_html!r}, expected text/html (nginx MIME config broken?)"
+ assert ct_txt.startswith(
+ "text/plain"
+ ), f"{txt_name} Content-Type={ct_txt!r}, expected text/plain (nginx MIME config broken?)"
diff --git a/tests/custom-html/test_install.py b/tests/custom-html/test_install.py
index 4a5c409..353eeb5 100644
--- a/tests/custom-html/test_install.py
+++ b/tests/custom-html/test_install.py
@@ -9,7 +9,8 @@ import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
-from harness import browser as harness_browser, generic # noqa: E402
+from harness import browser as harness_browser # noqa: E402
+from harness import generic
def test_serving_and_content(live_app, meta):
diff --git a/tests/discourse/functional/_discourse.py b/tests/discourse/functional/_discourse.py
index 203e4ee..d548bd0 100644
--- a/tests/discourse/functional/_discourse.py
+++ b/tests/discourse/functional/_discourse.py
@@ -53,7 +53,7 @@ def mint_admin(domain: str) -> tuple[str, str]:
cmd = (
"cd /opt/bitnami/discourse && "
"RUBY=$(command -v ruby || echo /opt/bitnami/ruby/bin/ruby) && "
- f"RAILS_ENV=production \"$RUBY\" bin/rails runner \"{_BOOTSTRAP_RB}\""
+ f'RAILS_ENV=production "$RUBY" bin/rails runner "{_BOOTSTRAP_RB}"'
)
out = lifecycle.exec_in_app(domain, ["bash", "-c", cmd], service="app", timeout=240)
key = user = None
@@ -63,9 +63,9 @@ def mint_admin(domain: str) -> tuple[str, str]:
key = line.split("=", 1)[1].strip()
elif line.startswith("CCCI_API_USER="):
user = line.split("=", 1)[1].strip()
- assert key and user, (
- f"could not bootstrap discourse admin/API key; rails output tail:\n{out[-1000:]}"
- )
+ assert (
+ key and user
+ ), f"could not bootstrap discourse admin/API key; rails output tail:\n{out[-1000:]}"
return key, user
diff --git a/tests/discourse/functional/test_create_topic.py b/tests/discourse/functional/test_create_topic.py
index fbadcef..4b078a5 100644
--- a/tests/discourse/functional/test_create_topic.py
+++ b/tests/discourse/functional/test_create_topic.py
@@ -48,21 +48,23 @@ def test_create_topic_roundtrip(live_app):
headers=hdrs,
timeout=60,
)
- assert status in (200, 201) and isinstance(body, dict), (
- f"create topic failed: HTTP {status}, body={body!r}"
- )
+ assert status in (200, 201) and isinstance(
+ body, dict
+ ), f"create topic failed: HTTP {status}, body={body!r}"
topic_id = body.get("topic_id")
assert topic_id, f"create topic returned no topic_id: {body!r}"
# 4) Read the topic back and assert title + first-post body round-trip.
status, got = harness_http.http_get(f"{base}/t/{topic_id}.json", headers=hdrs, timeout=30)
- assert status == 200 and isinstance(got, dict), f"read topic failed: HTTP {status}, body={got!r}"
- assert got.get("title") == title, (
- f"topic title did not round-trip: sent {title!r}, got {got.get('title')!r}"
- )
+ assert status == 200 and isinstance(
+ got, dict
+ ), f"read topic failed: HTTP {status}, body={got!r}"
+ assert (
+ got.get("title") == title
+ ), f"topic title did not round-trip: sent {title!r}, got {got.get('title')!r}"
posts = (got.get("post_stream") or {}).get("posts") or []
assert posts, f"topic has no posts on read-back: {got!r}"
first_cooked = posts[0].get("cooked", "")
- assert marker in first_cooked, (
- f"topic body did not round-trip: marker {marker!r} not in first post {first_cooked!r}"
- )
+ assert (
+ marker in first_cooked
+ ), f"topic body did not round-trip: marker {marker!r} not in first post {first_cooked!r}"
diff --git a/tests/discourse/functional/test_site_basic.py b/tests/discourse/functional/test_site_basic.py
index fd8793d..77414c2 100644
--- a/tests/discourse/functional/test_site_basic.py
+++ b/tests/discourse/functional/test_site_basic.py
@@ -20,12 +20,12 @@ def test_site_json_has_discourse_config(live_app):
status, body = harness_http.retry_http_get(
f"https://{live_app}/site.json", expect_status=200, max_wait=120, interval=5
)
- assert status == 200 and isinstance(body, dict), (
- f"GET /site.json failed: HTTP {status}, body type={type(body).__name__}"
- )
+ assert status == 200 and isinstance(
+ body, dict
+ ), f"GET /site.json failed: HTTP {status}, body type={type(body).__name__}"
# /site.json carries Discourse-specific structure — `categories` (a list) and `groups` are always
# present in a booted Discourse. A non-Discourse 200 (placeholder page) would not parse to this.
assert "categories" in body, f"/site.json missing 'categories' key: keys={list(body)[:20]}"
- assert isinstance(body["categories"], list), (
- f"/site.json 'categories' not a list: {type(body['categories']).__name__}"
- )
+ assert isinstance(
+ body["categories"], list
+ ), f"/site.json 'categories' not a list: {type(body['categories']).__name__}"
diff --git a/tests/discourse/ops.py b/tests/discourse/ops.py
index 73aa05d..5f619a7 100644
--- a/tests/discourse/ops.py
+++ b/tests/discourse/ops.py
@@ -15,8 +15,7 @@ from harness import lifecycle # noqa: E402
def _psql(domain, sql):
cmd = (
- 'PGPASSWORD=$(cat /run/secrets/db_password) '
- f'psql -U discourse -d discourse -tAc "{sql}"'
+ "PGPASSWORD=$(cat /run/secrets/db_password) " f'psql -U discourse -d discourse -tAc "{sql}"'
)
return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip()
@@ -42,6 +41,7 @@ def pre_backup(domain, meta):
def pre_restore(domain, meta):
# diverge from the backup so a successful restore is observable
_psql(domain, "DROP TABLE IF EXISTS ci_marker;")
- assert _psql(domain, "SELECT to_regclass('public.ci_marker');") in ("", "NULL"), (
- "drop did not take"
- )
+ assert _psql(domain, "SELECT to_regclass('public.ci_marker');") in (
+ "",
+ "NULL",
+ ), "drop did not take"
diff --git a/tests/discourse/recipe_meta.py b/tests/discourse/recipe_meta.py
index 2af8ff6..9092875 100644
--- a/tests/discourse/recipe_meta.py
+++ b/tests/discourse/recipe_meta.py
@@ -6,7 +6,9 @@
# app is actually serving (the canonical "is discourse up" signal — NOT "/", which may redirect to setup).
HEALTH_PATH = "/srv/status"
HEALTH_OK = (200,)
-DEPLOY_TIMEOUT = 3600 # slow Rails cold boot (15-25min) on the 7-GiB single node; bumped 2400→3600 for
+DEPLOY_TIMEOUT = (
+ 3600 # slow Rails cold boot (15-25min) on the 7-GiB single node; bumped 2400→3600 for
+)
# headroom after full4's base deploy timed out at 2400s (RAM/CPU-constrained boot + image re-pull).
HTTP_TIMEOUT = 1200
@@ -59,7 +61,11 @@ def BACKUP_VERIFY(domain):
try:
out = lifecycle.exec_in_app(
domain,
- ["sh", "-c", "gzip -t /var/lib/postgresql/data/backup.sql && wc -c < /var/lib/postgresql/data/backup.sql"],
+ [
+ "sh",
+ "-c",
+ "gzip -t /var/lib/postgresql/data/backup.sql && wc -c < /var/lib/postgresql/data/backup.sql",
+ ],
service="db",
timeout=60,
).strip()
diff --git a/tests/discourse/test_backup.py b/tests/discourse/test_backup.py
index 1955078..003dad3 100644
--- a/tests/discourse/test_backup.py
+++ b/tests/discourse/test_backup.py
@@ -14,13 +14,12 @@ from harness import lifecycle # noqa: E402
def _psql(domain, sql):
cmd = (
- 'PGPASSWORD=$(cat /run/secrets/db_password) '
- f'psql -U discourse -d discourse -tAc "{sql}"'
+ "PGPASSWORD=$(cat /run/secrets/db_password) " f'psql -U discourse -d discourse -tAc "{sql}"'
)
return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip()
def test_backup_captures_state(live_app):
- assert _psql(live_app, "SELECT v FROM ci_marker;") == "original", (
- "the seeded discourse postgres state was not present at backup time"
- )
+ assert (
+ _psql(live_app, "SELECT v FROM ci_marker;") == "original"
+ ), "the seeded discourse postgres state was not present at backup time"
diff --git a/tests/discourse/test_restore.py b/tests/discourse/test_restore.py
index 7137028..f93b9e7 100644
--- a/tests/discourse/test_restore.py
+++ b/tests/discourse/test_restore.py
@@ -14,13 +14,12 @@ from harness import lifecycle # noqa: E402
def _psql(domain, sql):
cmd = (
- 'PGPASSWORD=$(cat /run/secrets/db_password) '
- f'psql -U discourse -d discourse -tAc "{sql}"'
+ "PGPASSWORD=$(cat /run/secrets/db_password) " f'psql -U discourse -d discourse -tAc "{sql}"'
)
return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip()
def test_restore_returns_state(live_app):
- assert _psql(live_app, "SELECT v FROM ci_marker;") == "original", (
- "restore did not return the pre-mutation discourse postgres state (data-integrity failure)"
- )
+ assert (
+ _psql(live_app, "SELECT v FROM ci_marker;") == "original"
+ ), "restore did not return the pre-mutation discourse postgres state (data-integrity failure)"
diff --git a/tests/ghost/functional/_ghost.py b/tests/ghost/functional/_ghost.py
index 22a2e30..5147099 100644
--- a/tests/ghost/functional/_ghost.py
+++ b/tests/ghost/functional/_ghost.py
@@ -93,9 +93,10 @@ class GhostAdmin:
status, body = self.req(
"POST", "/session/", {"username": ADMIN_EMAIL, "password": ADMIN_PW}
)
- assert status in (200, 201), (
- f"ghost admin session login failed: HTTP {status}, body={body!r}"
- )
+ assert status in (
+ 200,
+ 201,
+ ), f"ghost admin session login failed: HTTP {status}, body={body!r}"
def create_post(self, title: str, html: str) -> dict:
status, body = self.req(
diff --git a/tests/ghost/functional/test_admin_redirect.py b/tests/ghost/functional/test_admin_redirect.py
index 2331a10..5403546 100644
--- a/tests/ghost/functional/test_admin_redirect.py
+++ b/tests/ghost/functional/test_admin_redirect.py
@@ -53,13 +53,15 @@ def test_ghost_admin_route_is_wired(live_app):
return None
status_body = harness_http.assert_converges(
- _ready, f"GET {url} returns Ghost admin (200) or setup redirect (302)",
- max_wait=60, interval=3,
+ _ready,
+ f"GET {url} returns Ghost admin (200) or setup redirect (302)",
+ max_wait=60,
+ interval=3,
)
status, body = status_body
assert status in (200, 302), f"unexpected status: {status}"
if status == 200:
# The admin SPA references /ghost-assets/ or contains "ghost" in title/body
- assert "ghost" in body.lower(), (
- f"GET {url} 200 but body has no Ghost markers: {body[:200]!r}"
- )
+ assert (
+ "ghost" in body.lower()
+ ), f"GET {url} 200 but body has no Ghost markers: {body[:200]!r}"
diff --git a/tests/ghost/functional/test_content_api.py b/tests/ghost/functional/test_content_api.py
index 32feaab..3c8282a 100644
--- a/tests/ghost/functional/test_content_api.py
+++ b/tests/ghost/functional/test_content_api.py
@@ -35,10 +35,10 @@ def test_content_api_settings_endpoint(live_app):
assert body is not None, f"GET {url} returned non-JSON body"
# On success: {"settings": {...}}. On error: {"errors": [...]}. Either shape is valid.
if status == 200:
- assert isinstance(body, dict) and "settings" in body, (
- f"200 response missing 'settings' envelope: {body!r}"
- )
+ assert (
+ isinstance(body, dict) and "settings" in body
+ ), f"200 response missing 'settings' envelope: {body!r}"
else:
- assert isinstance(body, dict) and ("errors" in body or "message" in body or body), (
- f"error response not a proper Ghost error envelope: {body!r}"
- )
+ assert isinstance(body, dict) and (
+ "errors" in body or "message" in body or body
+ ), f"error response not a proper Ghost error envelope: {body!r}"
diff --git a/tests/ghost/functional/test_post_roundtrip.py b/tests/ghost/functional/test_post_roundtrip.py
index ccb1916..f61f84d 100644
--- a/tests/ghost/functional/test_post_roundtrip.py
+++ b/tests/ghost/functional/test_post_roundtrip.py
@@ -43,17 +43,17 @@ def test_create_post_roundtrip(live_app):
title = f"ccci-marker-{uniq}"
marker = f"ccci-body-marker-{uniq}-roundtrip"
created = admin.create_post(title, f"{marker}
")
- assert created.get("title") == title, (
- f"created post title mismatch: sent {title!r}, got {created.get('title')!r}"
- )
+ assert (
+ created.get("title") == title
+ ), f"created post title mismatch: sent {title!r}, got {created.get('title')!r}"
# 4) Read it back by id and assert the post survived the round-trip (title always returned;
# html returned because we requested ?formats=html).
got = admin.get_post(created["id"])
- assert got.get("title") == title, (
- f"post title did not round-trip: sent {title!r}, got {got.get('title')!r}"
- )
+ assert (
+ got.get("title") == title
+ ), f"post title did not round-trip: sent {title!r}, got {got.get('title')!r}"
html = got.get("html") or ""
- assert marker in html, (
- f"post body did not round-trip: marker {marker!r} not in read-back html {html!r}"
- )
+ assert (
+ marker in html
+ ), f"post body did not round-trip: marker {marker!r} not in read-back html {html!r}"
diff --git a/tests/ghost/ops.py b/tests/ghost/ops.py
index da1d782..24448ba 100644
--- a/tests/ghost/ops.py
+++ b/tests/ghost/ops.py
@@ -22,10 +22,7 @@ from harness import lifecycle # noqa: E402
def _mysql(domain, sql):
- cmd = (
- 'MYSQL_PWD="$(cat /run/secrets/db_password)" '
- f'mysql -u root -N -s ghost -e "{sql}"'
- )
+ cmd = 'MYSQL_PWD="$(cat /run/secrets/db_password)" ' f'mysql -u root -N -s ghost -e "{sql}"'
return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip()
diff --git a/tests/ghost/recipe_meta.py b/tests/ghost/recipe_meta.py
index 5543f0a..710f50f 100644
--- a/tests/ghost/recipe_meta.py
+++ b/tests/ghost/recipe_meta.py
@@ -63,7 +63,11 @@ def BACKUP_VERIFY(domain):
try:
out = lifecycle.exec_in_app(
domain,
- ["sh", "-c", "gzip -t /var/lib/mysql/backup.sql.gz && wc -c < /var/lib/mysql/backup.sql.gz"],
+ [
+ "sh",
+ "-c",
+ "gzip -t /var/lib/mysql/backup.sql.gz && wc -c < /var/lib/mysql/backup.sql.gz",
+ ],
service="db",
timeout=60,
).strip()
diff --git a/tests/ghost/test_backup.py b/tests/ghost/test_backup.py
index 27ece25..4f1a5ea 100644
--- a/tests/ghost/test_backup.py
+++ b/tests/ghost/test_backup.py
@@ -15,14 +15,11 @@ from harness import lifecycle # noqa: E402
def _mysql(domain, sql):
- cmd = (
- 'MYSQL_PWD="$(cat /run/secrets/db_password)" '
- f'mysql -u root -N -s ghost -e "{sql}"'
- )
+ cmd = 'MYSQL_PWD="$(cat /run/secrets/db_password)" ' f'mysql -u root -N -s ghost -e "{sql}"'
return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip()
def test_backup_captures_state(live_app):
- assert _mysql(live_app, "SELECT v FROM ci_marker;") == "original", (
- "the seeded ghost MySQL marker was not present at backup time"
- )
+ assert (
+ _mysql(live_app, "SELECT v FROM ci_marker;") == "original"
+ ), "the seeded ghost MySQL marker was not present at backup time"
diff --git a/tests/ghost/test_restore.py b/tests/ghost/test_restore.py
index 413e73e..2e0fbc3 100644
--- a/tests/ghost/test_restore.py
+++ b/tests/ghost/test_restore.py
@@ -22,10 +22,7 @@ from harness import lifecycle # noqa: E402
def _mysql(domain, sql):
- cmd = (
- 'MYSQL_PWD="$(cat /run/secrets/db_password)" '
- f'mysql -u root -N -s ghost -e "{sql}"'
- )
+ cmd = 'MYSQL_PWD="$(cat /run/secrets/db_password)" ' f'mysql -u root -N -s ghost -e "{sql}"'
return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip()
diff --git a/tests/ghost/test_upgrade.py b/tests/ghost/test_upgrade.py
index bb4efd8..efebbdb 100644
--- a/tests/ghost/test_upgrade.py
+++ b/tests/ghost/test_upgrade.py
@@ -14,14 +14,11 @@ from harness import lifecycle # noqa: E402
def _mysql(domain, sql):
- cmd = (
- 'MYSQL_PWD="$(cat /run/secrets/db_password)" '
- f'mysql -u root -N -s ghost -e "{sql}"'
- )
+ cmd = 'MYSQL_PWD="$(cat /run/secrets/db_password)" ' f'mysql -u root -N -s ghost -e "{sql}"'
return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip()
def test_upgrade_preserves_state(live_app):
- assert _mysql(live_app, "SELECT v FROM ci_marker;") == "upgrade-survives", (
- "the seeded ghost MySQL marker did not survive the upgrade redeploy (data loss on upgrade)"
- )
+ assert (
+ _mysql(live_app, "SELECT v FROM ci_marker;") == "upgrade-survives"
+ ), "the seeded ghost MySQL marker did not survive the upgrade redeploy (data loss on upgrade)"
diff --git a/tests/hedgedoc/functional/test_branding.py b/tests/hedgedoc/functional/test_branding.py
index 2a87a23..bd13820 100644
--- a/tests/hedgedoc/functional/test_branding.py
+++ b/tests/hedgedoc/functional/test_branding.py
@@ -14,7 +14,6 @@ import urllib.request
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner"))
from harness import http as harness_http # noqa: E402
-
_CTX = ssl.create_default_context()
_CTX.check_hostname = False
_CTX.verify_mode = ssl.CERT_NONE
diff --git a/tests/hedgedoc/functional/test_health_check.py b/tests/hedgedoc/functional/test_health_check.py
index 4676345..067e2da 100644
--- a/tests/hedgedoc/functional/test_health_check.py
+++ b/tests/hedgedoc/functional/test_health_check.py
@@ -15,7 +15,5 @@ from harness import http as harness_http # noqa: E402
def test_hedgedoc_root_serves(live_app):
"""GET / → 200 or 302 (login/new redirect)."""
url = f"https://{live_app}/"
- status, _ = harness_http.retry_http_get(
- url, expect_status=(200, 302), max_wait=90, interval=5
- )
+ status, _ = harness_http.retry_http_get(url, expect_status=(200, 302), max_wait=90, interval=5)
assert status in (200, 302), f"GET {url} HTTP {status} (expected 200 or 302)"
diff --git a/tests/immich/functional/test_asset_processing.py b/tests/immich/functional/test_asset_processing.py
index b5df995..d637308 100644
--- a/tests/immich/functional/test_asset_processing.py
+++ b/tests/immich/functional/test_asset_processing.py
@@ -111,13 +111,13 @@ def test_immich_processes_uploaded_asset_metadata_and_statistics(live_app):
if exif and exif.get("exifImageWidth"):
break
time.sleep(5)
- assert exif and exif.get("exifImageWidth") == 1 and exif.get("exifImageHeight") == 1, (
- f"immich metadata-extraction did not populate the 1x1 PNG dimensions in exifInfo: {exif!r}"
- )
+ assert (
+ exif and exif.get("exifImageWidth") == 1 and exif.get("exifImageHeight") == 1
+ ), f"immich metadata-extraction did not populate the 1x1 PNG dimensions in exifInfo: {exif!r}"
# the asset is catalogued into the owner's library statistics (list-back in aggregate)
sst, stats = harness_http.http_request("GET", f"{base}/api/assets/statistics", headers=auth)
assert sst == 200 and isinstance(stats, dict), f"statistics HTTP {sst}: {stats!r}"
- assert stats.get("images", 0) >= 1 and stats.get("total", 0) >= 1, (
- f"uploaded asset not reflected in library statistics: {stats!r}"
- )
+ assert (
+ stats.get("images", 0) >= 1 and stats.get("total", 0) >= 1
+ ), f"uploaded asset not reflected in library statistics: {stats!r}"
diff --git a/tests/immich/functional/test_asset_upload.py b/tests/immich/functional/test_asset_upload.py
index 05b7131..0339401 100644
--- a/tests/immich/functional/test_asset_upload.py
+++ b/tests/immich/functional/test_asset_upload.py
@@ -121,6 +121,6 @@ def test_immich_upload_asset_readback_and_thumbnail(live_app):
if thumb == 200:
break
time.sleep(5)
- assert thumb == 200, (
- f"immich did not generate a thumbnail/derivative for the uploaded asset (last HTTP {thumb})"
- )
+ assert (
+ thumb == 200
+ ), f"immich did not generate a thumbnail/derivative for the uploaded asset (last HTTP {thumb})"
diff --git a/tests/immich/functional/test_health_check.py b/tests/immich/functional/test_health_check.py
index cc5534e..c5b05ab 100644
--- a/tests/immich/functional/test_health_check.py
+++ b/tests/immich/functional/test_health_check.py
@@ -16,5 +16,11 @@ from harness import http as harness_http # noqa: E402
def test_immich_returns_200(live_app):
url = f"https://{live_app}/"
- status, _ = harness_http.retry_http_get(url, expect_status=(200, 301, 302), max_wait=60, interval=3)
- assert status in (200, 301, 302), f"immich at {url} returned HTTP {status} (expected 200/301/302)"
+ status, _ = harness_http.retry_http_get(
+ url, expect_status=(200, 301, 302), max_wait=60, interval=3
+ )
+ assert status in (
+ 200,
+ 301,
+ 302,
+ ), f"immich at {url} returned HTTP {status} (expected 200/301/302)"
diff --git a/tests/immich/ops.py b/tests/immich/ops.py
index a0bdb03..daa4d7d 100644
--- a/tests/immich/ops.py
+++ b/tests/immich/ops.py
@@ -35,4 +35,7 @@ def pre_backup(domain, meta):
def pre_restore(domain, meta):
_psql(domain, "DROP TABLE ci_marker;")
- assert _psql(domain, "SELECT to_regclass('public.ci_marker');") in ("", "NULL"), "drop did not take"
+ assert _psql(domain, "SELECT to_regclass('public.ci_marker');") in (
+ "",
+ "NULL",
+ ), "drop did not take"
diff --git a/tests/immich/test_backup.py b/tests/immich/test_backup.py
index e0a8af5..1dc8756 100644
--- a/tests/immich/test_backup.py
+++ b/tests/immich/test_backup.py
@@ -14,4 +14,6 @@ def _psql(domain, sql):
def test_backup_captures_state(live_app):
- assert _psql(live_app, "SELECT v FROM ci_marker;") == "original", "seeded postgres state not present at backup time"
+ assert (
+ _psql(live_app, "SELECT v FROM ci_marker;") == "original"
+ ), "seeded postgres state not present at backup time"
diff --git a/tests/immich/test_install.py b/tests/immich/test_install.py
index 7552ff2..9785107 100644
--- a/tests/immich/test_install.py
+++ b/tests/immich/test_install.py
@@ -7,7 +7,8 @@ import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
-from harness import browser as harness_browser, generic, lifecycle # noqa: E402
+from harness import browser as harness_browser # noqa: E402
+from harness import generic, lifecycle
def test_serving_and_frontend(live_app, meta):
@@ -25,7 +26,11 @@ def test_serving_and_frontend(live_app, meta):
resp = harness_browser.goto_with_retry(
page, url, accept_statuses=(200, 301, 302), goto_timeout_ms=60_000
)
- assert resp is not None and resp.status in (200, 301, 302), f"page status {resp and resp.status}"
+ assert resp is not None and resp.status in (
+ 200,
+ 301,
+ 302,
+ ), f"page status {resp and resp.status}"
assert "-<6hex>" so concurrent dependents never collide.
- assert re.fullmatch(r"lasuite-docs-[0-9a-f]{6}", kc["realm"]), (
- f"realm {kc['realm']!r} not the per-run namespaced form lasuite-docs-<6hex>"
- )
+ assert re.fullmatch(
+ r"lasuite-docs-[0-9a-f]{6}", kc["realm"]
+ ), f"realm {kc['realm']!r} not the per-run namespaced form lasuite-docs-<6hex>"
assert kc["client_id"] == "lasuite-docs"
assert isinstance(kc["client_secret"], str) and len(kc["client_secret"]) >= 16
assert isinstance(kc["password"], str) and len(kc["password"]) >= 16
@@ -74,16 +74,14 @@ def test_oidc_password_grant_against_dep_keycloak(live_app, deps_creds):
# Password grant → real JWT
token = sso.oidc_password_grant(creds)
- assert isinstance(token, str) and token.count(".") == 2, (
- f"access_token is not a JWT: {token!r}"
- )
+ assert isinstance(token, str) and token.count(".") == 2, f"access_token is not a JWT: {token!r}"
payload = json.loads(_b64url_decode(token.split(".")[1]))
assert payload.get("iss") == expected_iss, f"JWT iss={payload.get('iss')!r} != {expected_iss!r}"
- assert payload.get("azp") == kc["client_id"], (
- f"JWT azp={payload.get('azp')!r} != {kc['client_id']!r}"
- )
+ assert (
+ payload.get("azp") == kc["client_id"]
+ ), f"JWT azp={payload.get('azp')!r} != {kc['client_id']!r}"
assert payload.get("typ") == "Bearer", f"JWT typ={payload.get('typ')!r} != 'Bearer'"
exp = payload.get("exp")
- assert isinstance(exp, int) and exp > time.time(), (
- f"JWT exp={exp!r} not a future timestamp (now={time.time():.0f})"
- )
+ assert (
+ isinstance(exp, int) and exp > time.time()
+ ), f"JWT exp={exp!r} not a future timestamp (now={time.time():.0f})"
diff --git a/tests/lasuite-docs/setup_custom_tests.sh b/tests/lasuite-docs/setup_custom_tests.sh
index 469f783..8f61331 100755
--- a/tests/lasuite-docs/setup_custom_tests.sh
+++ b/tests/lasuite-docs/setup_custom_tests.sh
@@ -21,15 +21,24 @@ set -euo pipefail
: "${CCCI_APP_DOMAIN:?missing}"
: "${CCCI_DEPS_FILE:?missing}"
-test -s "$CCCI_DEPS_FILE" || { echo " setup_custom_tests: deps file empty"; exit 1; }
+test -s "$CCCI_DEPS_FILE" || {
+ echo " setup_custom_tests: deps file empty"
+ exit 1
+}
# Read keycloak dep info via jq
-KC_DOMAIN=$(jq -r '.keycloak.domain' "$CCCI_DEPS_FILE")
-KC_REALM=$( jq -r '.keycloak.realm' "$CCCI_DEPS_FILE")
-KC_CLIENT=$(jq -r '.keycloak.client_id' "$CCCI_DEPS_FILE")
-KC_SECRET=$(jq -r '.keycloak.client_secret' "$CCCI_DEPS_FILE")
-[ -n "$KC_DOMAIN" ] && [ "$KC_DOMAIN" != "null" ] || { echo " setup_custom_tests: no keycloak.domain in deps"; exit 1; }
-[ -n "$KC_SECRET" ] && [ "$KC_SECRET" != "null" ] || { echo " setup_custom_tests: no keycloak.client_secret"; exit 1; }
+KC_DOMAIN=$(jq -r '.keycloak.domain' "$CCCI_DEPS_FILE")
+KC_REALM=$(jq -r '.keycloak.realm' "$CCCI_DEPS_FILE")
+KC_CLIENT=$(jq -r '.keycloak.client_id' "$CCCI_DEPS_FILE")
+KC_SECRET=$(jq -r '.keycloak.client_secret' "$CCCI_DEPS_FILE")
+if [ -z "$KC_DOMAIN" ] || [ "$KC_DOMAIN" = "null" ]; then
+ echo " setup_custom_tests: no keycloak.domain in deps"
+ exit 1
+fi
+if [ -z "$KC_SECRET" ] || [ "$KC_SECRET" = "null" ]; then
+ echo " setup_custom_tests: no keycloak.client_secret"
+ exit 1
+fi
echo " lasuite-docs setup_custom_tests: wiring OIDC against keycloak dep ${KC_DOMAIN}"
@@ -39,12 +48,15 @@ echo " lasuite-docs setup_custom_tests: wiring OIDC against keycloak dep ${KC_D
# update SECRET_OIDC_RPCS_VERSION in the .env to point at the new one.
ENV_PATH="$HOME/.abra/servers/default/${CCCI_APP_DOMAIN}.env"
CUR_VER=$(grep -E '^\s*SECRET_OIDC_RPCS_VERSION=' "$ENV_PATH" | tail -1 | cut -d= -f2 | tr -d '"\r' || echo "v1")
-NEW_NUM=$(( ${CUR_VER#v} + 1 ))
+NEW_NUM=$((${CUR_VER#v} + 1))
NEW_VER="v${NEW_NUM}"
-INSERT_LOG=$(abra app secret insert $CCCI_APP_DOMAIN oidc_rpcs $NEW_VER $KC_SECRET --no-input -C -o 2>&1) \
- || INSERT_LOG=$(script -qec "abra app secret insert $CCCI_APP_DOMAIN oidc_rpcs $NEW_VER $KC_SECRET --no-input -C -o" /dev/null 2>&1) \
- || { echo " setup_custom_tests: abra app secret insert oidc_rpcs@$NEW_VER failed: $INSERT_LOG"; exit 1; }
+INSERT_LOG=$(abra app secret insert "$CCCI_APP_DOMAIN" oidc_rpcs "$NEW_VER" "$KC_SECRET" --no-input -C -o 2>&1) ||
+ INSERT_LOG=$(script -qec "abra app secret insert $CCCI_APP_DOMAIN oidc_rpcs $NEW_VER $KC_SECRET --no-input -C -o" /dev/null 2>&1) ||
+ {
+ echo " setup_custom_tests: abra app secret insert oidc_rpcs@$NEW_VER failed: $INSERT_LOG"
+ exit 1
+ }
# Repoint the env var to the new version
sed -i "s|^\s*SECRET_OIDC_RPCS_VERSION=.*|SECRET_OIDC_RPCS_VERSION=$NEW_VER|" "$ENV_PATH"
echo " setup_custom_tests: oidc_rpcs secret inserted at $NEW_VER (was $CUR_VER)"
@@ -52,25 +64,25 @@ echo " setup_custom_tests: oidc_rpcs secret inserted at $NEW_VER (was $CUR_VER)
# 2) Write OIDC env vars to the app's .env (names per lasuite-docs's .env.sample).
# Ensure the file ends with a newline FIRST so our appends don't concatenate onto the last line
# (we saw `TIMEOUT=900OIDC_REALM=...` malformed by a missing-trailing-newline file).
-[ -z "$(tail -c1 "$ENV_PATH" 2>/dev/null)" ] || printf '\n' >> "$ENV_PATH"
-write_env () {
+[ -z "$(tail -c1 "$ENV_PATH" 2>/dev/null)" ] || printf '\n' >>"$ENV_PATH"
+write_env() {
local key="$1" val="$2"
# remove any existing key (commented or live) then append the live key=val
sed -i "/^\s*#\?\s*${key}=/d" "$ENV_PATH"
# Re-ensure trailing newline after each delete (sed may leave the file without one)
- [ -z "$(tail -c1 "$ENV_PATH" 2>/dev/null)" ] || printf '\n' >> "$ENV_PATH"
- printf '%s=%s\n' "$key" "$val" >> "$ENV_PATH"
+ [ -z "$(tail -c1 "$ENV_PATH" 2>/dev/null)" ] || printf '\n' >>"$ENV_PATH"
+ printf '%s=%s\n' "$key" "$val" >>"$ENV_PATH"
}
-write_env OIDC_REALM "$KC_REALM"
-write_env OIDC_OP_DISCOVERY_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/.well-known/openid-configuration"
-write_env OIDC_OP_AUTHORIZATION_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/auth"
-write_env OIDC_OP_TOKEN_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/token"
-write_env OIDC_OP_USER_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/userinfo"
-write_env OIDC_OP_LOGOUT_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/logout"
-write_env OIDC_OP_JWKS_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/certs"
-write_env OIDC_RP_CLIENT_ID "$KC_CLIENT"
-write_env OIDC_RP_SIGN_ALGO "RS256"
-write_env OIDC_RP_SCOPES "openid email profile"
+write_env OIDC_REALM "$KC_REALM"
+write_env OIDC_OP_DISCOVERY_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/.well-known/openid-configuration"
+write_env OIDC_OP_AUTHORIZATION_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/auth"
+write_env OIDC_OP_TOKEN_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/token"
+write_env OIDC_OP_USER_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/userinfo"
+write_env OIDC_OP_LOGOUT_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/logout"
+write_env OIDC_OP_JWKS_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/certs"
+write_env OIDC_RP_CLIENT_ID "$KC_CLIENT"
+write_env OIDC_RP_SIGN_ALGO "RS256"
+write_env OIDC_RP_SCOPES "openid email profile"
# 3) Trigger an in-place redeploy so the env update takes effect. --force re-deploys even when
# the recipe hasn't changed; --chaos avoids the chaos prompt; --no-input non-interactive.
diff --git a/tests/lasuite-docs/test_install.py b/tests/lasuite-docs/test_install.py
index 9bf94e3..fa35a11 100644
--- a/tests/lasuite-docs/test_install.py
+++ b/tests/lasuite-docs/test_install.py
@@ -10,7 +10,8 @@ import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
-from harness import browser as harness_browser, generic, lifecycle # noqa: E402
+from harness import browser as harness_browser # noqa: E402
+from harness import generic, lifecycle
def test_serving_and_frontend(live_app, meta):
diff --git a/tests/lasuite-drive/functional/test_health_check.py b/tests/lasuite-drive/functional/test_health_check.py
index 0be8b14..06ec229 100644
--- a/tests/lasuite-drive/functional/test_health_check.py
+++ b/tests/lasuite-drive/functional/test_health_check.py
@@ -25,6 +25,8 @@ def test_lasuite_drive_returns_200(live_app):
status, _ = harness_http.retry_http_get(
url, expect_status=(200, 301, 302), max_wait=60, interval=3
)
- assert status in (200, 301, 302), (
- f"lasuite-drive at {url} returned HTTP {status} (expected 200/301/302)"
- )
+ assert status in (
+ 200,
+ 301,
+ 302,
+ ), f"lasuite-drive at {url} returned HTTP {status} (expected 200/301/302)"
diff --git a/tests/lasuite-drive/functional/test_minio_storage.py b/tests/lasuite-drive/functional/test_minio_storage.py
index 3c3d15a..b11d22d 100644
--- a/tests/lasuite-drive/functional/test_minio_storage.py
+++ b/tests/lasuite-drive/functional/test_minio_storage.py
@@ -29,8 +29,8 @@ BUCKET = "drive-media-storage"
def _mc(domain: str, script: str) -> str:
"""Run an `mc` shell script inside the minio container (root creds from /run/secrets)."""
prelude = (
- 'set -e; '
- 'U=$(cat /run/secrets/minio_ru); P=$(cat /run/secrets/minio_rp); '
+ "set -e; "
+ "U=$(cat /run/secrets/minio_ru); P=$(cat /run/secrets/minio_rp); "
'mc alias set ccci http://localhost:9000 "$U" "$P" >/dev/null 2>&1; '
)
return lifecycle.exec_in_app(domain, ["sh", "-c", prelude + script], service="minio")
@@ -49,13 +49,13 @@ def test_minio_bucket_present_and_object_roundtrip(live_app):
domain,
# upload via stdin; list the object; read it back (tagged); then delete.
f'printf %s "{marker}" | mc pipe ccci/{BUCKET}/{key} >/dev/null 2>&1; '
- f'mc ls ccci/{BUCKET}/{key}; '
+ f"mc ls ccci/{BUCKET}/{key}; "
f'echo "READBACK:$(mc cat ccci/{BUCKET}/{key})"; '
- f'mc rm ccci/{BUCKET}/{key} >/dev/null 2>&1',
+ f"mc rm ccci/{BUCKET}/{key} >/dev/null 2>&1",
)
# The object was listed (its key appears) and its content round-tripped intact.
assert f"{marker}.txt" in out, f"uploaded object not listed in bucket: {out!r}"
- assert f"READBACK:{marker}" in out, (
- f"object content did not round-trip through MinIO; got: {out!r}"
- )
+ assert (
+ f"READBACK:{marker}" in out
+ ), f"object content did not round-trip through MinIO; got: {out!r}"
diff --git a/tests/lasuite-drive/functional/test_oidc_with_keycloak.py b/tests/lasuite-drive/functional/test_oidc_with_keycloak.py
index eb2820f..e2aaef7 100644
--- a/tests/lasuite-drive/functional/test_oidc_with_keycloak.py
+++ b/tests/lasuite-drive/functional/test_oidc_with_keycloak.py
@@ -46,9 +46,9 @@ def test_oidc_password_grant_against_dep_keycloak(live_app, deps_creds):
# Creds shape. WC1: realm is per-run namespaced "-<6hex>"; client_id stays the parent.
assert kc["domain"]
- assert re.fullmatch(r"lasuite-drive-[0-9a-f]{6}", kc["realm"]), (
- f"realm {kc['realm']!r} not the per-run namespaced form lasuite-drive-<6hex>"
- )
+ assert re.fullmatch(
+ r"lasuite-drive-[0-9a-f]{6}", kc["realm"]
+ ), f"realm {kc['realm']!r} not the per-run namespaced form lasuite-drive-<6hex>"
assert kc["client_id"] == "lasuite-drive"
assert isinstance(kc["client_secret"], str) and len(kc["client_secret"]) >= 16
assert isinstance(kc["password"], str) and len(kc["password"]) >= 16
@@ -77,16 +77,14 @@ def test_oidc_password_grant_against_dep_keycloak(live_app, deps_creds):
# Password grant → real JWT
token = sso.oidc_password_grant(creds)
- assert isinstance(token, str) and token.count(".") == 2, (
- f"access_token is not a JWT: {token!r}"
- )
+ assert isinstance(token, str) and token.count(".") == 2, f"access_token is not a JWT: {token!r}"
payload = json.loads(_b64url_decode(token.split(".")[1]))
assert payload.get("iss") == expected_iss, f"JWT iss={payload.get('iss')!r} != {expected_iss!r}"
- assert payload.get("azp") == kc["client_id"], (
- f"JWT azp={payload.get('azp')!r} != {kc['client_id']!r}"
- )
+ assert (
+ payload.get("azp") == kc["client_id"]
+ ), f"JWT azp={payload.get('azp')!r} != {kc['client_id']!r}"
assert payload.get("typ") == "Bearer", f"JWT typ={payload.get('typ')!r} != 'Bearer'"
exp = payload.get("exp")
- assert isinstance(exp, int) and exp > time.time(), (
- f"JWT exp={exp!r} not a future timestamp (now={time.time():.0f})"
- )
+ assert (
+ isinstance(exp, int) and exp > time.time()
+ ), f"JWT exp={exp!r} not a future timestamp (now={time.time():.0f})"
diff --git a/tests/lasuite-drive/install_steps.sh b/tests/lasuite-drive/install_steps.sh
index 7ce2a52..c864ff3 100755
--- a/tests/lasuite-drive/install_steps.sh
+++ b/tests/lasuite-drive/install_steps.sh
@@ -28,7 +28,7 @@ if [ -z "${CCCI_DEPS_FILE:-}" ] || [ ! -s "${CCCI_DEPS_FILE}" ]; then
exit 0
fi
KC_DOMAIN=$(jq -r '.keycloak.domain // empty' "$CCCI_DEPS_FILE")
-KC_REALM=$( jq -r '.keycloak.realm // empty' "$CCCI_DEPS_FILE")
+KC_REALM=$(jq -r '.keycloak.realm // empty' "$CCCI_DEPS_FILE")
KC_CLIENT=$(jq -r '.keycloak.client_id // empty' "$CCCI_DEPS_FILE")
KC_SECRET=$(jq -r '.keycloak.client_secret // empty' "$CCCI_DEPS_FILE")
if [ -z "$KC_DOMAIN" ] || [ -z "$KC_SECRET" ]; then
@@ -43,35 +43,38 @@ echo " lasuite-drive install_steps: wiring OIDC at install against keycloak ${K
# point SECRET_OIDC_RPCS_VERSION at it. (The app is not deployed yet — a swarm secret can be created
# independently of a running stack — so the single deploy below picks up v2.)
CUR_VER=$(grep -E '^\s*SECRET_OIDC_RPCS_VERSION=' "$ENV_PATH" | tail -1 | cut -d= -f2 | tr -d '"\r' || echo "v1")
-NEW_NUM=$(( ${CUR_VER#v} + 1 ))
+NEW_NUM=$((${CUR_VER#v} + 1))
NEW_VER="v${NEW_NUM}"
-INSERT_LOG=$(abra app secret insert "$CCCI_APP_DOMAIN" oidc_rpcs "$NEW_VER" "$KC_SECRET" --no-input -C -o 2>&1) \
- || INSERT_LOG=$(script -qec "abra app secret insert $CCCI_APP_DOMAIN oidc_rpcs $NEW_VER $KC_SECRET --no-input -C -o" /dev/null 2>&1) \
- || { echo " install_steps: abra app secret insert oidc_rpcs@$NEW_VER failed: $INSERT_LOG"; exit 1; }
+INSERT_LOG=$(abra app secret insert "$CCCI_APP_DOMAIN" oidc_rpcs "$NEW_VER" "$KC_SECRET" --no-input -C -o 2>&1) ||
+ INSERT_LOG=$(script -qec "abra app secret insert $CCCI_APP_DOMAIN oidc_rpcs $NEW_VER $KC_SECRET --no-input -C -o" /dev/null 2>&1) ||
+ {
+ echo " install_steps: abra app secret insert oidc_rpcs@$NEW_VER failed: $INSERT_LOG"
+ exit 1
+ }
sed -i "s|^\s*SECRET_OIDC_RPCS_VERSION=.*|SECRET_OIDC_RPCS_VERSION=$NEW_VER|" "$ENV_PATH"
echo " install_steps: oidc_rpcs secret inserted at $NEW_VER (was $CUR_VER)"
# 2) Write the OIDC env vars (explicit endpoints — deterministic, no reliance on ${AUTH_DOMAIN}
# expansion). Mirrors the recipe-maintainer impress/La Suite OIDC env contract.
-write_env () {
+write_env() {
local key="$1" val="$2"
sed -i "/^\s*#\?\s*${key}=/d" "$ENV_PATH"
- [ -z "$(tail -c1 "$ENV_PATH" 2>/dev/null)" ] || printf '\n' >> "$ENV_PATH"
- printf '%s=%s\n' "$key" "$val" >> "$ENV_PATH"
+ [ -z "$(tail -c1 "$ENV_PATH" 2>/dev/null)" ] || printf '\n' >>"$ENV_PATH"
+ printf '%s=%s\n' "$key" "$val" >>"$ENV_PATH"
}
-write_env AUTH_DOMAIN "$KC_DOMAIN"
-write_env OIDC_REALM "$KC_REALM"
-write_env OIDC_OP_JWKS_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/certs"
-write_env OIDC_OP_AUTHORIZATION_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/auth"
-write_env OIDC_OP_TOKEN_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/token"
-write_env OIDC_OP_USER_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/userinfo"
-write_env OIDC_OP_LOGOUT_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/logout"
-write_env OIDC_RP_CLIENT_ID "$KC_CLIENT"
-write_env OIDC_RP_SIGN_ALGO "RS256"
-write_env OIDC_RP_SCOPES "openid email profile"
-write_env OIDC_REDIRECT_ALLOWED_HOSTS "[\"https://${KC_DOMAIN}\", \"https://${CCCI_APP_DOMAIN}\"]"
+write_env AUTH_DOMAIN "$KC_DOMAIN"
+write_env OIDC_REALM "$KC_REALM"
+write_env OIDC_OP_JWKS_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/certs"
+write_env OIDC_OP_AUTHORIZATION_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/auth"
+write_env OIDC_OP_TOKEN_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/token"
+write_env OIDC_OP_USER_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/userinfo"
+write_env OIDC_OP_LOGOUT_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/logout"
+write_env OIDC_RP_CLIENT_ID "$KC_CLIENT"
+write_env OIDC_RP_SIGN_ALGO "RS256"
+write_env OIDC_RP_SCOPES "openid email profile"
+write_env OIDC_REDIRECT_ALLOWED_HOSTS "[\"https://${KC_DOMAIN}\", \"https://${CCCI_APP_DOMAIN}\"]"
# The recipe default acr_values=eidas1 is FranceConnect-specific; keycloak can't satisfy it and it
# would break the interactive auth flow. Clear it so the keycloak OIDC client works.
-write_env OIDC_AUTH_REQUEST_EXTRA_PARAMS "{}"
+write_env OIDC_AUTH_REQUEST_EXTRA_PARAMS "{}"
echo " lasuite-drive install_steps: OIDC env wired into .env (deploy will pick it up, no reconverge)"
diff --git a/tests/lasuite-drive/setup_custom_tests.sh b/tests/lasuite-drive/setup_custom_tests.sh
index 1bd3d34..65d84f2 100755
--- a/tests/lasuite-drive/setup_custom_tests.sh
+++ b/tests/lasuite-drive/setup_custom_tests.sh
@@ -29,7 +29,7 @@ docker service scale --detach "${STACK}_minio-createbuckets=1" >/dev/null 2>&1 |
for i in $(seq 1 30); do
MC_CID=$(docker ps -q -f "name=${STACK}_minio.1" | head -1)
if [ -n "$MC_CID" ] && docker exec "$MC_CID" sh -c \
- 'mc alias set _c http://localhost:9000 "$(cat /run/secrets/minio_ru)" "$(cat /run/secrets/minio_rp)" >/dev/null 2>&1 && mc ls _c/drive-media-storage >/dev/null 2>&1'; then
+ 'mc alias set _c http://localhost:9000 "$(cat /run/secrets/minio_ru)" "$(cat /run/secrets/minio_rp)" >/dev/null 2>&1 && mc ls _c/drive-media-storage >/dev/null 2>&1'; then
echo " setup: bucket drive-media-storage present after ${i} poll(s)"
break
fi
diff --git a/tests/lasuite-drive/test_install.py b/tests/lasuite-drive/test_install.py
index 6115bff..b0f7869 100644
--- a/tests/lasuite-drive/test_install.py
+++ b/tests/lasuite-drive/test_install.py
@@ -10,7 +10,8 @@ import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
-from harness import browser as harness_browser, generic, lifecycle # noqa: E402
+from harness import browser as harness_browser # noqa: E402
+from harness import generic, lifecycle
def test_serving_and_frontend(live_app, meta):
diff --git a/tests/lasuite-meet/functional/test_health_check.py b/tests/lasuite-meet/functional/test_health_check.py
index b854916..c7e2132 100644
--- a/tests/lasuite-meet/functional/test_health_check.py
+++ b/tests/lasuite-meet/functional/test_health_check.py
@@ -21,6 +21,8 @@ def test_lasuite_meet_returns_200(live_app):
status, _ = harness_http.retry_http_get(
url, expect_status=(200, 301, 302), max_wait=60, interval=3
)
- assert status in (200, 301, 302), (
- f"lasuite-meet at {url} returned HTTP {status} (expected 200/301/302)"
- )
+ assert status in (
+ 200,
+ 301,
+ 302,
+ ), f"lasuite-meet at {url} returned HTTP {status} (expected 200/301/302)"
diff --git a/tests/lasuite-meet/functional/test_meeting_flow.py b/tests/lasuite-meet/functional/test_meeting_flow.py
index b34156e..333f6a4 100644
--- a/tests/lasuite-meet/functional/test_meeting_flow.py
+++ b/tests/lasuite-meet/functional/test_meeting_flow.py
@@ -28,7 +28,8 @@ import sys
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner"))
-from harness import http as harness_http, sso # noqa: E402
+from harness import http as harness_http # noqa: E402
+from harness import sso
def _b64url(seg: str) -> bytes:
@@ -74,33 +75,40 @@ def test_create_room_get_livekit_token_and_read_back(live_app, deps_creds):
lk_room = livekit.get("room")
lk_token = livekit.get("token")
assert room_id, f"room created but no id: {body!r}"
- assert lk_token and isinstance(lk_token, str) and lk_token.count(".") == 2, (
- f"room created but no LiveKit JWT token: {livekit!r}"
- )
+ assert (
+ lk_token and isinstance(lk_token, str) and lk_token.count(".") == 2
+ ), f"room created but no LiveKit JWT token: {livekit!r}"
try:
# --- read it back (a fresh authenticated GET of the created room) ---
- status, got = harness_http.http_request("GET", f"{base}/api/v1.0/rooms/{room_id}/", headers=auth)
+ status, got = harness_http.http_request(
+ "GET", f"{base}/api/v1.0/rooms/{room_id}/", headers=auth
+ )
assert status == 200, f"room read-back returned HTTP {status} (expected 200); body={got!r}"
- assert isinstance(got, dict) and got.get("id") == room_id, (
- f"read-back room id mismatch: {got!r}"
- )
- got_lk = (got.get("livekit") or {})
+ assert (
+ isinstance(got, dict) and got.get("id") == room_id
+ ), f"read-back room id mismatch: {got!r}"
+ got_lk = got.get("livekit") or {}
assert got_lk.get("token"), f"read-back room missing LiveKit token: {got!r}"
- assert got_lk.get("room") == lk_room, (
- f"read-back LiveKit room {got_lk.get('room')!r} != create-time {lk_room!r}"
- )
+ assert (
+ got_lk.get("room") == lk_room
+ ), f"read-back LiveKit room {got_lk.get('room')!r} != create-time {lk_room!r}"
# --- the LiveKit token is a real signaling grant for this room (WebRTC subset) ---
payload = json.loads(_b64url(lk_token.split(".")[1]))
video = payload.get("video") or {}
- assert video.get("room") == lk_room or payload.get("room") == lk_room, (
- f"LiveKit JWT does not grant the created room {lk_room!r}: {payload!r}"
- )
+ assert (
+ video.get("room") == lk_room or payload.get("room") == lk_room
+ ), f"LiveKit JWT does not grant the created room {lk_room!r}: {payload!r}"
finally:
# --- delete the room (cleanup + a real DELETE mutation) ---
- del_status, _ = harness_http.http_request("DELETE", f"{base}/api/v1.0/rooms/{room_id}/", headers=auth)
- assert del_status in (204, 200), f"room delete returned HTTP {del_status} (expected 204/200)"
+ del_status, _ = harness_http.http_request(
+ "DELETE", f"{base}/api/v1.0/rooms/{room_id}/", headers=auth
+ )
+ assert del_status in (
+ 204,
+ 200,
+ ), f"room delete returned HTTP {del_status} (expected 204/200)"
# --- best-effort: confirm the delete took (404 on re-GET). The §4.3 floor (create-an-object +
# read-it-back + LiveKit-token issuance) is already proven by the hard assertions above; this
@@ -112,7 +120,9 @@ def test_create_room_get_livekit_token_and_read_back(live_app, deps_creds):
gone = False
for _ in range(5):
- status, _ = harness_http.http_request("GET", f"{base}/api/v1.0/rooms/{room_id}/", headers=auth)
+ status, _ = harness_http.http_request(
+ "GET", f"{base}/api/v1.0/rooms/{room_id}/", headers=auth
+ )
if status == 404:
gone = True
break
diff --git a/tests/lasuite-meet/functional/test_oidc_with_keycloak.py b/tests/lasuite-meet/functional/test_oidc_with_keycloak.py
index ef59d54..3335d2c 100644
--- a/tests/lasuite-meet/functional/test_oidc_with_keycloak.py
+++ b/tests/lasuite-meet/functional/test_oidc_with_keycloak.py
@@ -46,9 +46,9 @@ def test_oidc_password_grant_against_dep_keycloak(live_app, deps_creds):
# Creds shape. WC1: realm is per-run namespaced "-<6hex>"; client_id stays the parent.
assert kc["domain"]
- assert re.fullmatch(r"lasuite-meet-[0-9a-f]{6}", kc["realm"]), (
- f"realm {kc['realm']!r} not the per-run namespaced form lasuite-meet-<6hex>"
- )
+ assert re.fullmatch(
+ r"lasuite-meet-[0-9a-f]{6}", kc["realm"]
+ ), f"realm {kc['realm']!r} not the per-run namespaced form lasuite-meet-<6hex>"
assert kc["client_id"] == "lasuite-meet"
assert isinstance(kc["client_secret"], str) and len(kc["client_secret"]) >= 16
assert isinstance(kc["password"], str) and len(kc["password"]) >= 16
@@ -77,16 +77,14 @@ def test_oidc_password_grant_against_dep_keycloak(live_app, deps_creds):
# Password grant → real JWT
token = sso.oidc_password_grant(creds)
- assert isinstance(token, str) and token.count(".") == 2, (
- f"access_token is not a JWT: {token!r}"
- )
+ assert isinstance(token, str) and token.count(".") == 2, f"access_token is not a JWT: {token!r}"
payload = json.loads(_b64url_decode(token.split(".")[1]))
assert payload.get("iss") == expected_iss, f"JWT iss={payload.get('iss')!r} != {expected_iss!r}"
- assert payload.get("azp") == kc["client_id"], (
- f"JWT azp={payload.get('azp')!r} != {kc['client_id']!r}"
- )
+ assert (
+ payload.get("azp") == kc["client_id"]
+ ), f"JWT azp={payload.get('azp')!r} != {kc['client_id']!r}"
assert payload.get("typ") == "Bearer", f"JWT typ={payload.get('typ')!r} != 'Bearer'"
exp = payload.get("exp")
- assert isinstance(exp, int) and exp > time.time(), (
- f"JWT exp={exp!r} not a future timestamp (now={time.time():.0f})"
- )
+ assert (
+ isinstance(exp, int) and exp > time.time()
+ ), f"JWT exp={exp!r} not a future timestamp (now={time.time():.0f})"
diff --git a/tests/lasuite-meet/install_steps.sh b/tests/lasuite-meet/install_steps.sh
index 73b02a4..8d310eb 100755
--- a/tests/lasuite-meet/install_steps.sh
+++ b/tests/lasuite-meet/install_steps.sh
@@ -26,7 +26,7 @@ if [ -z "${CCCI_DEPS_FILE:-}" ] || [ ! -s "${CCCI_DEPS_FILE}" ]; then
exit 0
fi
KC_DOMAIN=$(jq -r '.keycloak.domain // empty' "$CCCI_DEPS_FILE")
-KC_REALM=$( jq -r '.keycloak.realm // empty' "$CCCI_DEPS_FILE")
+KC_REALM=$(jq -r '.keycloak.realm // empty' "$CCCI_DEPS_FILE")
KC_CLIENT=$(jq -r '.keycloak.client_id // empty' "$CCCI_DEPS_FILE")
KC_SECRET=$(jq -r '.keycloak.client_secret // empty' "$CCCI_DEPS_FILE")
if [ -z "$KC_DOMAIN" ] || [ -z "$KC_SECRET" ]; then
@@ -40,31 +40,34 @@ echo " lasuite-meet install_steps: wiring OIDC at install against keycloak ${KC
# forbids overwriting a secret at the same version). The app is not deployed yet — a swarm secret can
# be created independently — so the single deploy below picks up v2.
CUR_VER=$(grep -E '^\s*SECRET_OIDC_RPCS_VERSION=' "$ENV_PATH" | tail -1 | cut -d= -f2 | tr -d '"\r' || echo "v1")
-NEW_NUM=$(( ${CUR_VER#v} + 1 ))
+NEW_NUM=$((${CUR_VER#v} + 1))
NEW_VER="v${NEW_NUM}"
-INSERT_LOG=$(abra app secret insert "$CCCI_APP_DOMAIN" oidc_rpcs "$NEW_VER" "$KC_SECRET" --no-input -C -o 2>&1) \
- || INSERT_LOG=$(script -qec "abra app secret insert $CCCI_APP_DOMAIN oidc_rpcs $NEW_VER $KC_SECRET --no-input -C -o" /dev/null 2>&1) \
- || { echo " install_steps: abra app secret insert oidc_rpcs@$NEW_VER failed: $INSERT_LOG"; exit 1; }
+INSERT_LOG=$(abra app secret insert "$CCCI_APP_DOMAIN" oidc_rpcs "$NEW_VER" "$KC_SECRET" --no-input -C -o 2>&1) ||
+ INSERT_LOG=$(script -qec "abra app secret insert $CCCI_APP_DOMAIN oidc_rpcs $NEW_VER $KC_SECRET --no-input -C -o" /dev/null 2>&1) ||
+ {
+ echo " install_steps: abra app secret insert oidc_rpcs@$NEW_VER failed: $INSERT_LOG"
+ exit 1
+ }
sed -i "s|^\s*SECRET_OIDC_RPCS_VERSION=.*|SECRET_OIDC_RPCS_VERSION=$NEW_VER|" "$ENV_PATH"
echo " install_steps: oidc_rpcs secret inserted at $NEW_VER (was $CUR_VER)"
# 2) Write the OIDC env vars (explicit endpoints — deterministic). Meet's .env.sample templates the
# endpoints off ${AUTH_DOMAIN}; set AUTH_DOMAIN + override each endpoint with the concrete realm URL.
-write_env () {
+write_env() {
local key="$1" val="$2"
sed -i "/^\s*#\?\s*${key}=/d" "$ENV_PATH"
- [ -z "$(tail -c1 "$ENV_PATH" 2>/dev/null)" ] || printf '\n' >> "$ENV_PATH"
- printf '%s=%s\n' "$key" "$val" >> "$ENV_PATH"
+ [ -z "$(tail -c1 "$ENV_PATH" 2>/dev/null)" ] || printf '\n' >>"$ENV_PATH"
+ printf '%s=%s\n' "$key" "$val" >>"$ENV_PATH"
}
-write_env AUTH_DOMAIN "$KC_DOMAIN"
-write_env OIDC_REALM "$KC_REALM"
-write_env OIDC_OP_JWKS_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/certs"
-write_env OIDC_OP_AUTHORIZATION_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/auth"
-write_env OIDC_OP_TOKEN_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/token"
-write_env OIDC_OP_USER_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/userinfo"
-write_env OIDC_OP_LOGOUT_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/logout"
-write_env OIDC_RP_CLIENT_ID "$KC_CLIENT"
-write_env OIDC_RP_SIGN_ALGO "RS256"
-write_env OIDC_RP_SCOPES "openid email"
+write_env AUTH_DOMAIN "$KC_DOMAIN"
+write_env OIDC_REALM "$KC_REALM"
+write_env OIDC_OP_JWKS_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/certs"
+write_env OIDC_OP_AUTHORIZATION_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/auth"
+write_env OIDC_OP_TOKEN_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/token"
+write_env OIDC_OP_USER_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/userinfo"
+write_env OIDC_OP_LOGOUT_ENDPOINT "https://${KC_DOMAIN}/realms/${KC_REALM}/protocol/openid-connect/logout"
+write_env OIDC_RP_CLIENT_ID "$KC_CLIENT"
+write_env OIDC_RP_SIGN_ALGO "RS256"
+write_env OIDC_RP_SCOPES "openid email"
echo " lasuite-meet install_steps: OIDC env wired into .env (deploy will pick it up, no reconverge)"
diff --git a/tests/lasuite-meet/test_install.py b/tests/lasuite-meet/test_install.py
index d2e4d40..fe9c686 100644
--- a/tests/lasuite-meet/test_install.py
+++ b/tests/lasuite-meet/test_install.py
@@ -10,7 +10,8 @@ import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
-from harness import browser as harness_browser, generic, lifecycle # noqa: E402
+from harness import browser as harness_browser # noqa: E402
+from harness import generic, lifecycle
def test_serving_and_frontend(live_app, meta):
@@ -33,9 +34,11 @@ def test_serving_and_frontend(live_app, meta):
resp = harness_browser.goto_with_retry(
page, url, accept_statuses=(200, 301, 302), goto_timeout_ms=60_000
)
- assert resp is not None and resp.status in (200, 301, 302), (
- f"page status {resp and resp.status}"
- )
+ assert resp is not None and resp.status in (
+ 200,
+ 301,
+ 302,
+ ), f"page status {resp and resp.status}"
assert " 0, (
- f"server.version is not a non-empty string: {version!r}"
- )
+ assert (
+ isinstance(version, str) and len(version) > 0
+ ), f"server.version is not a non-empty string: {version!r}"
diff --git a/tests/matrix-synapse/functional/test_health_check.py b/tests/matrix-synapse/functional/test_health_check.py
index e655c41..01f1e10 100644
--- a/tests/matrix-synapse/functional/test_health_check.py
+++ b/tests/matrix-synapse/functional/test_health_check.py
@@ -11,7 +11,6 @@ Runs in the custom tier against the shared post-install live deployment.
from __future__ import annotations
-import json
import os
import sys
@@ -24,6 +23,6 @@ def test_synapse_client_versions_returns_json(live_app):
url = f"https://{live_app}/_matrix/client/versions"
status, body = harness_http.retry_http_get(url, expect_status=200, max_wait=60, interval=3)
assert status == 200, f"GET {url} HTTP {status} (expected 200)"
- assert isinstance(body, dict) and isinstance(body.get("versions"), list) and body["versions"], (
- f"GET {url} did not return Matrix client-versions document: {body!r}"
- )
+ assert (
+ isinstance(body, dict) and isinstance(body.get("versions"), list) and body["versions"]
+ ), f"GET {url} did not return Matrix client-versions document: {body!r}"
diff --git a/tests/matrix-synapse/functional/test_register_and_message.py b/tests/matrix-synapse/functional/test_register_and_message.py
index a00249e..b5c8067 100644
--- a/tests/matrix-synapse/functional/test_register_and_message.py
+++ b/tests/matrix-synapse/functional/test_register_and_message.py
@@ -42,7 +42,8 @@ import sys
import uuid
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner"))
-from harness import http as harness_http, lifecycle # noqa: E402
+from harness import http as harness_http # noqa: E402
+from harness import lifecycle
def _registration_secret(domain: str) -> str:
@@ -101,8 +102,11 @@ def _admin_register(domain: str, secret: str, username: str, password: str, admi
r = _container_curl(domain, "GET", "/_synapse/admin/v1/register")
if r["status"] in (500, 502, 503, 504, 0):
last = r
- print(f" [register] {username}: nonce GET transient {r['status']} "
- f"(attempt {attempt}, synapse recovering) — retrying", flush=True)
+ print(
+ f" [register] {username}: nonce GET transient {r['status']} "
+ f"(attempt {attempt}, synapse recovering) — retrying",
+ flush=True,
+ )
time.sleep(5)
continue
assert r["status"] == 200, f"nonce GET failed: status={r['status']} raw={r['raw'][:200]!r}"
@@ -122,13 +126,19 @@ def _admin_register(domain: str, secret: str, username: str, password: str, admi
r = _container_curl(domain, "POST", "/_synapse/admin/v1/register", body=payload)
if r["status"] == 200:
if attempt > 1:
- print(f" [register] {username}: succeeded on attempt {attempt} "
- f"(synapse recovered)", flush=True)
+ print(
+ f" [register] {username}: succeeded on attempt {attempt} "
+ f"(synapse recovered)",
+ flush=True,
+ )
return r["body"] or {}
if r["status"] in (500, 502, 503, 504, 0):
last = r
- print(f" [register] {username}: POST transient {r['status']} "
- f"(attempt {attempt}, synapse recovering) — retrying", flush=True)
+ print(
+ f" [register] {username}: POST transient {r['status']} "
+ f"(attempt {attempt}, synapse recovering) — retrying",
+ flush=True,
+ )
time.sleep(5)
continue
# a 4xx is a real rejection — fail fast, do not retry
@@ -167,9 +177,9 @@ def test_register_two_users_send_receive_message(live_app):
create + invite + join a room; send and read a message."""
domain = live_app
secret = _registration_secret(domain)
- assert secret and len(secret) >= 16, (
- f"registration shared secret missing/short: len={len(secret) if secret else 0}"
- )
+ assert (
+ secret and len(secret) >= 16
+ ), f"registration shared secret missing/short: len={len(secret) if secret else 0}"
suffix = uuid.uuid4().hex[:8]
user_a = f"alice{suffix}"
diff --git a/tests/mattermost-lts/functional/test_create_message.py b/tests/mattermost-lts/functional/test_create_message.py
index a3efdd0..0c066a2 100644
--- a/tests/mattermost-lts/functional/test_create_message.py
+++ b/tests/mattermost-lts/functional/test_create_message.py
@@ -41,9 +41,9 @@ def test_create_message_roundtrip(live_app):
headers=auth,
timeout=30,
)
- assert status in (200, 201) and isinstance(team, dict) and team.get("id"), (
- f"team creation failed: HTTP {status}, body={team!r}"
- )
+ assert (
+ status in (200, 201) and isinstance(team, dict) and team.get("id")
+ ), f"team creation failed: HTTP {status}, body={team!r}"
status, chan = harness_http.http_post(
f"{base}/channels",
data={
@@ -55,9 +55,9 @@ def test_create_message_roundtrip(live_app):
headers=auth,
timeout=30,
)
- assert status in (200, 201) and isinstance(chan, dict) and chan.get("id"), (
- f"channel creation failed: HTTP {status}, body={chan!r}"
- )
+ assert (
+ status in (200, 201) and isinstance(chan, dict) and chan.get("id")
+ ), f"channel creation failed: HTTP {status}, body={chan!r}"
# 4) POST a unique marker message.
marker = f"ccci-marker-{uniq}-roundtrip"
@@ -67,13 +67,13 @@ def test_create_message_roundtrip(live_app):
headers=auth,
timeout=30,
)
- assert status in (200, 201) and isinstance(post, dict) and post.get("id"), (
- f"post creation failed: HTTP {status}, body={post!r}"
- )
+ assert (
+ status in (200, 201) and isinstance(post, dict) and post.get("id")
+ ), f"post creation failed: HTTP {status}, body={post!r}"
# 5) Read it back by id and assert the message survived the round-trip.
status, got = harness_http.http_get(f"{base}/posts/{post['id']}", headers=auth, timeout=30)
assert status == 200 and isinstance(got, dict), f"read-back failed: HTTP {status}, body={got!r}"
- assert got.get("message") == marker, (
- f"message did not round-trip: sent {marker!r}, got {got.get('message')!r}"
- )
+ assert (
+ got.get("message") == marker
+ ), f"message did not round-trip: sent {marker!r}, got {got.get('message')!r}"
diff --git a/tests/mattermost-lts/functional/test_health_check.py b/tests/mattermost-lts/functional/test_health_check.py
index abdfcbe..605caa2 100644
--- a/tests/mattermost-lts/functional/test_health_check.py
+++ b/tests/mattermost-lts/functional/test_health_check.py
@@ -18,9 +18,7 @@ from harness import http as harness_http # noqa: E402
def test_root_serves(live_app):
"""GET / → 200 or 302 (mattermost web app shell / login redirect)."""
url = f"https://{live_app}/"
- status, _ = harness_http.retry_http_get(
- url, expect_status=(200, 302), max_wait=60, interval=3
- )
+ status, _ = harness_http.retry_http_get(url, expect_status=(200, 302), max_wait=60, interval=3)
assert status in (200, 302), f"GET {url} HTTP {status} (expected 200/302)"
@@ -28,10 +26,8 @@ def test_system_ping_ok(live_app):
"""GET /api/v4/system/ping → 200 with JSON {"status":"OK"} — the mattermost server's own
liveness endpoint (distinguishes a live mattermost API from a Traefik fallback / dead backend)."""
url = f"https://{live_app}/api/v4/system/ping"
- status, body = harness_http.retry_http_get(
- url, expect_status=200, max_wait=120, interval=3
- )
+ status, body = harness_http.retry_http_get(url, expect_status=200, max_wait=120, interval=3)
assert status == 200, f"GET {url} HTTP {status} (expected 200)"
- assert isinstance(body, dict) and body.get("status") == "OK", (
- f"/api/v4/system/ping did not report status=OK; got {body!r}"
- )
+ assert (
+ isinstance(body, dict) and body.get("status") == "OK"
+ ), f"/api/v4/system/ping did not report status=OK; got {body!r}"
diff --git a/tests/mattermost-lts/functional/test_multiuser_message.py b/tests/mattermost-lts/functional/test_multiuser_message.py
index b9903dc..387e436 100644
--- a/tests/mattermost-lts/functional/test_multiuser_message.py
+++ b/tests/mattermost-lts/functional/test_multiuser_message.py
@@ -51,7 +51,12 @@ def test_second_user_reads_first_users_message(live_app):
assert status in (200, 201) and team.get("id"), f"team create HTTP {status}: {team!r}"
status, chan = harness_http.http_post(
f"{base}/channels",
- data={"team_id": team["id"], "name": f"c{uniq}", "display_name": f"chan {uniq}", "type": "O"},
+ data={
+ "team_id": team["id"],
+ "name": f"c{uniq}",
+ "display_name": f"chan {uniq}",
+ "type": "O",
+ },
headers=auth_a,
timeout=30,
)
@@ -60,7 +65,10 @@ def test_second_user_reads_first_users_message(live_app):
# 2) user_a posts a unique marker
marker = f"ccci-multiuser-{uniq}"
status, post = harness_http.http_post(
- f"{base}/posts", data={"channel_id": chan["id"], "message": marker}, headers=auth_a, timeout=30
+ f"{base}/posts",
+ data={"channel_id": chan["id"], "message": marker},
+ headers=auth_a,
+ timeout=30,
)
assert status in (200, 201) and post.get("id"), f"post create HTTP {status}: {post!r}"
@@ -97,6 +105,6 @@ def test_second_user_reads_first_users_message(live_app):
# 5) user_b sees user_a's marker (cross-user delivery, not a self read-back)
messages = [p.get("message") for p in (posts.get("posts") or {}).values()]
- assert marker in messages, (
- f"user_b did not see user_a's message {marker!r} in the channel; saw {messages!r}"
- )
+ assert (
+ marker in messages
+ ), f"user_b did not see user_a's message {marker!r} in the channel; saw {messages!r}"
diff --git a/tests/mattermost-lts/test_install.py b/tests/mattermost-lts/test_install.py
index fd73f9b..e8a40b4 100644
--- a/tests/mattermost-lts/test_install.py
+++ b/tests/mattermost-lts/test_install.py
@@ -15,6 +15,4 @@ def test_serving_and_api(live_app, meta):
generic.assert_serving(live_app, meta)
# ... then the recipe-specific assertion: the mattermost REST liveness endpoint answers 200.
status = lifecycle.http_get(live_app, "/api/v4/system/ping")
- assert status == 200, (
- f"expected 200 from {live_app}/api/v4/system/ping, got {status}"
- )
+ assert status == 200, f"expected 200 from {live_app}/api/v4/system/ping, got {status}"
diff --git a/tests/mumble/functional/_mumble_proto.py b/tests/mumble/functional/_mumble_proto.py
index a98846c..daaa69e 100644
--- a/tests/mumble/functional/_mumble_proto.py
+++ b/tests/mumble/functional/_mumble_proto.py
@@ -12,6 +12,7 @@ cc-ci host (`mode: host`); tests run on-host via cc-ci-run, so they connect to 1
from __future__ import annotations
+import contextlib
import socket
import ssl
import struct
@@ -29,8 +30,14 @@ MSG_USERSTATE = 9
MSG_SERVERCONFIG = 24
REJECT_TYPES = {
- 0: "None", 1: "WrongVersion", 2: "InvalidUsername", 3: "WrongUserPW",
- 4: "WrongServerPW", 5: "UsernameInUse", 6: "ServerFull", 7: "NoCertificate",
+ 0: "None",
+ 1: "WrongVersion",
+ 2: "InvalidUsername",
+ 3: "WrongUserPW",
+ 4: "WrongServerPW",
+ 5: "UsernameInUse",
+ 6: "ServerFull",
+ 7: "NoCertificate",
8: "AuthenticatorFail",
}
@@ -81,7 +88,7 @@ def _dec_fields(data: bytes) -> dict:
off += 8
elif wire == 2:
length, off = _dec_varint(data, off)
- raw = data[off:off + length]
+ raw = data[off : off + length]
off += length
try:
value = raw.decode("utf-8")
@@ -120,9 +127,11 @@ def _recv(sock, timeout: float) -> tuple[int, bytes]:
def _build_version() -> bytes:
v = (1 << 16) | (5 << 8) | 0 # pretend client 1.5.0
- return (_enc_field_varint(1, v)
- + _enc_field_string(2, "cc-ci mumble probe 1.0")
- + _enc_field_string(3, "Linux"))
+ return (
+ _enc_field_varint(1, v)
+ + _enc_field_string(2, "cc-ci mumble probe 1.0")
+ + _enc_field_string(3, "Linux")
+ )
def _build_authenticate(username: str, password: str = "") -> bytes:
@@ -133,18 +142,29 @@ def _build_authenticate(username: str, password: str = "") -> bytes:
return payload
-def handshake(host: str = "127.0.0.1", port: int = PORT, username: str = "cc-ci-probe",
- password: str = "", timeout: float = 20.0) -> dict:
+def handshake(
+ host: str = "127.0.0.1",
+ port: int = PORT,
+ username: str = "cc-ci-probe",
+ password: str = "",
+ timeout: float = 20.0,
+) -> dict:
"""Full Mumble control-channel handshake. Returns a result dict:
- tls_connect (bool), server_version (dict|None), auth_accepted (bool), channels (list[str]),
- users (list[str]), server_sync (bool), welcome_text (str|None), server_config (dict),
- error (str|None).
+ tls_connect (bool), server_version (dict|None), auth_accepted (bool), channels (list[str]),
+ users (list[str]), server_sync (bool), welcome_text (str|None), server_config (dict),
+ error (str|None).
"""
result = {
- "tls_connect": False, "server_version": None, "auth_accepted": False,
- "channels": [], "users": [], "server_sync": False, "welcome_text": None,
- "server_config": {}, "error": None,
+ "tls_connect": False,
+ "server_version": None,
+ "auth_accepted": False,
+ "channels": [],
+ "users": [],
+ "server_sync": False,
+ "welcome_text": None,
+ "server_config": {},
+ "error": None,
}
raw = tls = None
try:
@@ -181,19 +201,21 @@ def handshake(host: str = "127.0.0.1", port: int = PORT, username: str = "cc-ci-
break
try:
msg_type, payload = _recv(tls, timeout=remaining)
- except (socket.timeout, ConnectionError):
+ except (TimeoutError, ConnectionError):
break
if msg_type == MSG_VERSION:
f = _dec_fields(payload)
v1 = f.get(1, 0)
result["server_version"] = {
"string": f"{(v1 >> 16) & 0xFF}.{(v1 >> 8) & 0xFF}.{v1 & 0xFF}",
- "release": f.get(2, ""), "os": f.get(3, ""),
+ "release": f.get(2, ""),
+ "os": f.get(3, ""),
}
elif msg_type == MSG_REJECT:
f = _dec_fields(payload)
- result["error"] = (f"Rejected: {REJECT_TYPES.get(f.get(1, 0), 'Unknown')} "
- f"— {f.get(2, '')}")
+ result["error"] = (
+ f"Rejected: {REJECT_TYPES.get(f.get(1, 0), 'Unknown')} " f"— {f.get(2, '')}"
+ )
return result
elif msg_type == MSG_CHANNELSTATE:
f = _dec_fields(payload)
@@ -209,9 +231,12 @@ def handshake(host: str = "127.0.0.1", port: int = PORT, username: str = "cc-ci-
# ServerConfig fields: 1 max_bandwidth, 2 welcome_text, 3 allow_html,
# 4 message_length, 5 image_message_length, 6 max_users
result["server_config"] = {
- "max_bandwidth": f.get(1), "welcome_text": f.get(2),
- "allow_html": f.get(3), "message_length": f.get(4),
- "image_message_length": f.get(5), "max_users": f.get(6),
+ "max_bandwidth": f.get(1),
+ "welcome_text": f.get(2),
+ "allow_html": f.get(3),
+ "message_length": f.get(4),
+ "image_message_length": f.get(5),
+ "max_users": f.get(6),
}
elif msg_type == MSG_SERVERSYNC:
f = _dec_fields(payload)
@@ -230,10 +255,8 @@ def handshake(host: str = "127.0.0.1", port: int = PORT, username: str = "cc-ci-
result["error"] = f"{type(e).__name__}: {e}"
finally:
if tls is not None:
- try:
+ with contextlib.suppress(OSError):
tls.shutdown(socket.SHUT_RDWR)
- except OSError:
- pass
tls.close()
elif raw is not None:
raw.close()
diff --git a/tests/mumble/functional/test_protocol_handshake.py b/tests/mumble/functional/test_protocol_handshake.py
index a0aa49f..7bc314a 100644
--- a/tests/mumble/functional/test_protocol_handshake.py
+++ b/tests/mumble/functional/test_protocol_handshake.py
@@ -25,7 +25,7 @@ def test_handshake_completes_with_channel_presence(live_app):
assert r["server_version"] is not None, "server did not send a Version message"
assert r["auth_accepted"], f"authentication not accepted — {r.get('error')}"
# Channel presence: the server must expose at least the root channel (beyond a bare TCP open).
- assert len(r["channels"]) >= 1, (
- f"server reported no channels (expected >=1 root channel) — {r!r}"
- )
+ assert (
+ len(r["channels"]) >= 1
+ ), f"server reported no channels (expected >=1 root channel) — {r!r}"
assert r["server_sync"], f"ServerSync handshake did not complete — {r.get('error')}"
diff --git a/tests/mumble/functional/test_server_config_limits.py b/tests/mumble/functional/test_server_config_limits.py
index a6e2fbb..c84f2aa 100644
--- a/tests/mumble/functional/test_server_config_limits.py
+++ b/tests/mumble/functional/test_server_config_limits.py
@@ -32,6 +32,7 @@ def test_configured_max_users_surfaces_in_serverconfig(live_app):
)
# allow_html defaults true in the recipe; assert it is present/boolean to prove the field set
# is the real ServerConfig (not an empty/garbled decode).
- assert cfg.get("allow_html") in (0, 1), (
- f"ServerConfig.allow_html unexpected: {cfg.get('allow_html')!r}"
- )
+ assert cfg.get("allow_html") in (
+ 0,
+ 1,
+ ), f"ServerConfig.allow_html unexpected: {cfg.get('allow_html')!r}"
diff --git a/tests/mumble/recipe_meta.py b/tests/mumble/recipe_meta.py
index 1b99031..228b5b9 100644
--- a/tests/mumble/recipe_meta.py
+++ b/tests/mumble/recipe_meta.py
@@ -25,10 +25,10 @@
# WELCOME_TEXT -> MUMBLE_CONFIG_WELCOMETEXT, surfaced in the ServerSync welcome_text.
# USERS -> MUMBLE_CONFIG_USERS (max users), surfaced in the ServerConfig.max_users.
-HEALTH_PATH = "/" # mumble-web client UI (present on both 0.2.0 base and 1.0.0 latest)
+HEALTH_PATH = "/" # mumble-web client UI (present on both 0.2.0 base and 1.0.0 latest)
HEALTH_OK = (200,)
-DEPLOY_TIMEOUT = 900 # two images to pull (mumble-server + mumble-web) on a cold node
+DEPLOY_TIMEOUT = 900 # two images to pull (mumble-server + mumble-web) on a cold node
HTTP_TIMEOUT = 300
# A unique, stable welcome-text marker the round-trip test asserts surfaces over the protocol.
diff --git a/tests/mumble/test_backup.py b/tests/mumble/test_backup.py
index 334f251..40c051a 100644
--- a/tests/mumble/test_backup.py
+++ b/tests/mumble/test_backup.py
@@ -23,6 +23,6 @@ def _sqlite(domain, sql):
def test_backup_captures_state(live_app):
- assert _sqlite(live_app, "SELECT v FROM ci_marker;") == "original", (
- "the seeded mumble sqlite marker was not present at backup time"
- )
+ assert (
+ _sqlite(live_app, "SELECT v FROM ci_marker;") == "original"
+ ), "the seeded mumble sqlite marker was not present at backup time"
diff --git a/tests/mumble/test_restore.py b/tests/mumble/test_restore.py
index 6cfa458..9606ad5 100644
--- a/tests/mumble/test_restore.py
+++ b/tests/mumble/test_restore.py
@@ -25,6 +25,6 @@ def _sqlite(domain, sql):
def test_restore_returns_state(live_app):
- assert _sqlite(live_app, "SELECT v FROM ci_marker;") == "original", (
- "restore did not return the pre-mutation mumble sqlite marker (data-integrity failure)"
- )
+ assert (
+ _sqlite(live_app, "SELECT v FROM ci_marker;") == "original"
+ ), "restore did not return the pre-mutation mumble sqlite marker (data-integrity failure)"
diff --git a/tests/n8n/functional/test_login_state.py b/tests/n8n/functional/test_login_state.py
index 7e6fbe7..4768442 100644
--- a/tests/n8n/functional/test_login_state.py
+++ b/tests/n8n/functional/test_login_state.py
@@ -91,6 +91,6 @@ def test_login_endpoint_returns_json(live_app):
assert body is not None, f"/rest/login returned no parseable JSON: state={state}"
# If it's a dict, it's the expected envelope; if it's a list, n8n shouldn't do that on this
# endpoint, but accept either; only reject obvious non-shapes.
- assert isinstance(body, (dict, list)), (
- f"/rest/login returned unexpected JSON type {type(body).__name__}: {body!r}"
- )
+ assert isinstance(
+ body, dict | list
+ ), f"/rest/login returned unexpected JSON type {type(body).__name__}: {body!r}"
diff --git a/tests/n8n/functional/test_rest_settings.py b/tests/n8n/functional/test_rest_settings.py
index 792a730..bb459fa 100644
--- a/tests/n8n/functional/test_rest_settings.py
+++ b/tests/n8n/functional/test_rest_settings.py
@@ -72,9 +72,9 @@ def test_rest_settings_returns_json_with_known_keys(live_app):
# (e.g. version 3.2.0+2.20.6).
assert isinstance(body, dict), f"/rest/settings returned non-dict JSON: {type(body).__name__}"
data = body.get("data") if "data" in body else body
- assert isinstance(data, dict), (
- f"/rest/settings response missing 'data' envelope: keys={list(body.keys())[:10]}"
- )
+ assert isinstance(
+ data, dict
+ ), f"/rest/settings response missing 'data' envelope: keys={list(body.keys())[:10]}"
# Bootstrap keys the editor SPA relies on across versions:
# - `userManagement`: the auth-mode dict (whether owner-setup is needed, smtp/email mode).
# - `defaultLocale`: i18n bootstrap; present on every n8n install.
diff --git a/tests/n8n/functional/test_workflow_roundtrip.py b/tests/n8n/functional/test_workflow_roundtrip.py
index d408696..ba8d866 100644
--- a/tests/n8n/functional/test_workflow_roundtrip.py
+++ b/tests/n8n/functional/test_workflow_roundtrip.py
@@ -136,7 +136,9 @@ def _get_workflow(domain: str, cookie_header: str, workflow_id) -> dict:
return json.loads(resp.read())
except urllib.error.HTTPError as e:
body = e.read().decode(errors="replace")
- raise AssertionError(f"GET /rest/workflows/{workflow_id} HTTP {e.code}: {body[:200]}") from e
+ raise AssertionError(
+ f"GET /rest/workflows/{workflow_id} HTTP {e.code}: {body[:200]}"
+ ) from e
def test_workflow_create_and_read_back(live_app):
@@ -172,18 +174,21 @@ def test_workflow_create_and_read_back(live_app):
# Read it back and prove the round-trip
fetched = _get_workflow(domain, cookie, workflow_id)
fpayload = fetched.get("data") if isinstance(fetched.get("data"), dict) else fetched
- assert fpayload.get("id") in (workflow_id, str(workflow_id)), (
- f"GET workflow id={fpayload.get('id')!r} != created id={workflow_id!r}"
- )
- assert fpayload.get("name") == name, (
- f"workflow name didn't round-trip: created={name!r}, fetched={fpayload.get('name')!r}"
- )
+ assert fpayload.get("id") in (
+ workflow_id,
+ str(workflow_id),
+ ), f"GET workflow id={fpayload.get('id')!r} != created id={workflow_id!r}"
+ assert (
+ fpayload.get("name") == name
+ ), f"workflow name didn't round-trip: created={name!r}, fetched={fpayload.get('name')!r}"
nodes = fpayload.get("nodes") or []
- assert isinstance(nodes, list) and len(nodes) == 1, (
- f"workflow nodes didn't round-trip: expected 1 node, got {len(nodes)}"
- )
+ assert (
+ isinstance(nodes, list) and len(nodes) == 1
+ ), f"workflow nodes didn't round-trip: expected 1 node, got {len(nodes)}"
node = nodes[0]
- assert node.get("type") == "n8n-nodes-base.manualTrigger", (
- f"node type didn't round-trip: {node.get('type')!r}"
- )
- assert node.get("name") == "Manual Trigger", f"node name didn't round-trip: {node.get('name')!r}"
+ assert (
+ node.get("type") == "n8n-nodes-base.manualTrigger"
+ ), f"node type didn't round-trip: {node.get('type')!r}"
+ assert (
+ node.get("name") == "Manual Trigger"
+ ), f"node name didn't round-trip: {node.get('name')!r}"
diff --git a/tests/n8n/test_install.py b/tests/n8n/test_install.py
index 9d614cd..3e6885a 100644
--- a/tests/n8n/test_install.py
+++ b/tests/n8n/test_install.py
@@ -8,7 +8,8 @@ import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
-from harness import browser as harness_browser, generic, lifecycle # noqa: E402
+from harness import browser as harness_browser # noqa: E402
+from harness import generic, lifecycle
def test_serving_and_editor(live_app, meta):
@@ -33,9 +34,10 @@ def test_serving_and_editor(live_app, meta):
ctx = browser.new_context(ignore_https_errors=True)
page = ctx.new_page()
resp = harness_browser.goto_with_retry(page, url, accept_statuses=(200, 304))
- assert resp is not None and resp.status in (200, 304), (
- f"page status {resp and resp.status}"
- )
+ assert resp is not None and resp.status in (
+ 200,
+ 304,
+ ), f"page status {resp and resp.status}"
body = page.content().lower()
assert "n8n" in body or "/ URLs
assert "" in b
assert "https://ci.example/runs/42/badge.svg" in b
- assert "(https://drone.x/cc-ci/42)" in b # links to the run
+ assert "(https://drone.x/cc-ci/42)" in b # links to the run
def test_result_comment_text_fallback_when_card_missing(monkeypatch):
# Render failed / not served → MUST degrade to text, never a broken image (R7).
monkeypatch.setattr(bridge, "artifact_available", lambda url: False)
- b = bridge.result_comment_body("hedgedoc", "abc1234def", "9", "https://drone.x/cc-ci/9", "failure")
- assert "summary.png" not in b # no image embed
- assert "![" not in b # no markdown image at all
+ b = bridge.result_comment_body(
+ "hedgedoc", "abc1234def", "9", "https://drone.x/cc-ci/9", "failure"
+ )
+ assert "summary.png" not in b # no image embed
+ assert "![" not in b # no markdown image at all
assert "❌" in b and "failure" in b
assert "https://drone.x/cc-ci/9" in b
def test_find_existing_comment_matches_marker(monkeypatch):
- monkeypatch.setattr(bridge, "list_comments", lambda fn, n: [
- {"id": 1, "body": "just a normal comment"},
- {"id": 2, "body": bridge.COMMENT_MARKER + "\n🌻 old run"},
- ])
+ monkeypatch.setattr(
+ bridge,
+ "list_comments",
+ lambda fn, n: [
+ {"id": 1, "body": "just a normal comment"},
+ {"id": 2, "body": bridge.COMMENT_MARKER + "\n🌻 old run"},
+ ],
+ )
assert bridge.find_existing_comment("org/repo", 5) == 2
diff --git a/tests/unit/test_canonical.py b/tests/unit/test_canonical.py
index f25dd77..d31a193 100644
--- a/tests/unit/test_canonical.py
+++ b/tests/unit/test_canonical.py
@@ -48,7 +48,9 @@ def test_is_enrolled_reads_flag(tmp_path, monkeypatch):
def test_registry_roundtrip(tmp_path, monkeypatch):
monkeypatch.setenv("CCCI_WARM_ROOT", str(tmp_path))
assert canonical.read_registry("custom-html") is None
- rec = canonical.write_registry("custom-html", version="1.10.0+x", commit="abc123", status="idle")
+ rec = canonical.write_registry(
+ "custom-html", version="1.10.0+x", commit="abc123", status="idle"
+ )
assert rec["domain"] == "warm-custom-html.ci.commoninternet.net"
assert rec["version"] == "1.10.0+x" and rec["commit"] == "abc123" and rec["status"] == "idle"
back = canonical.read_registry("custom-html")
@@ -66,9 +68,11 @@ def test_enrolled_recipes_scans_meta(tmp_path, monkeypatch):
fake_harness = tmp_path / "runner" / "harness"
fake_harness.mkdir(parents=True)
monkeypatch.setattr(canonical, "__file__", str(fake_harness / "canonical.py"))
- for name, body in (("aaa", "WARM_CANONICAL = True\n"),
- ("bbb", "DEPS=['x']\n"),
- ("ccc", "WARM_CANONICAL = True\n")):
+ for name, body in (
+ ("aaa", "WARM_CANONICAL = True\n"),
+ ("bbb", "DEPS=['x']\n"),
+ ("ccc", "WARM_CANONICAL = True\n"),
+ ):
d = tmp_path / "tests" / name
d.mkdir(parents=True)
(d / "recipe_meta.py").write_text(body)
@@ -85,12 +89,13 @@ def test_prune_stale_drops_deenrolled_only(tmp_path, monkeypatch):
# enrolled canonical (keep), de-enrolled canonical (prune), reconciler dir (keep), alerts (keep)
for name in ("keepme", "gone"):
(tmp_path / name).mkdir()
- (tmp_path / name / "canonical.json").write_text('{"recipe":"%s"}' % name)
- (tmp_path / "keycloak").mkdir(); (tmp_path / "keycloak" / "last_good").write_text("v1") # reconciler
+ (tmp_path / name / "canonical.json").write_text(f'{{"recipe":"{name}"}}')
+ (tmp_path / "keycloak").mkdir()
+ (tmp_path / "keycloak" / "last_good").write_text("v1") # reconciler
(tmp_path / "alerts").mkdir()
pruned = canonical.prune_stale()
assert pruned == ["gone"]
assert not (tmp_path / "gone").exists()
assert (tmp_path / "keepme").exists()
- assert (tmp_path / "keycloak").exists() # no canonical.json → not a canonical → kept
+ assert (tmp_path / "keycloak").exists() # no canonical.json → not a canonical → kept
assert (tmp_path / "alerts").exists()
diff --git a/tests/unit/test_dashboard.py b/tests/unit/test_dashboard.py
index 944f3e7..c1b17ff 100644
--- a/tests/unit/test_dashboard.py
+++ b/tests/unit/test_dashboard.py
@@ -12,9 +12,8 @@ import os
import sys
import tempfile
-_tok = tempfile.NamedTemporaryFile("w", delete=False, suffix=".tok")
-_tok.write("test-token")
-_tok.close()
+with tempfile.NamedTemporaryFile("w", delete=False, suffix=".tok") as _tok:
+ _tok.write("test-token")
os.environ["DRONE_TOKEN_FILE"] = _tok.name
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "dashboard"))
@@ -23,10 +22,17 @@ import dashboard # noqa: E402
def _row(**kw):
base = {
- "recipe": "custom-html", "status": "success", "number": 4, "ref": "db9a9502",
- "version": "db9a95024e9d", "level": 4, "level_cap_reason": "",
- "has_screenshot": True, "flags": {"clean_teardown": True, "no_secret_leak": True},
- "finished": 0, "url": "https://drone.x/cc-ci/4",
+ "recipe": "custom-html",
+ "status": "success",
+ "number": 4,
+ "ref": "db9a9502",
+ "version": "db9a95024e9d",
+ "level": 4,
+ "level_cap_reason": "",
+ "has_screenshot": True,
+ "flags": {"clean_teardown": True, "no_secret_leak": True},
+ "finished": 0,
+ "url": "https://drone.x/cc-ci/4",
}
base.update(kw)
return base
@@ -43,24 +49,33 @@ def test_level_color_ramp_and_fallback():
def test_overview_grid_mirrors_results():
out = dashboard.render_overview([_row()])
assert "custom-html" in out
- assert "level 4" in out # the corner level pill
- assert dashboard.level_color(4) in out # coloured by level
- assert "db9a95024e9d" in out # version from results.json
- assert "/runs/4/screenshot.png" in out # thumbnail
- assert "/runs/4/summary.png" in out # links to full card
- assert "/recipe/custom-html" in out # history link
+ assert "level 4" in out # the corner level pill
+ assert dashboard.level_color(4) in out # coloured by level
+ assert "db9a95024e9d" in out # version from results.json
+ assert "/runs/4/screenshot.png" in out # thumbnail
+ assert "/runs/4/summary.png" in out # links to full card
+ assert "/recipe/custom-html" in out # history link
assert "✔ teardown" in out and "✔ no-leak" in out
def test_overview_never_greener_than_data():
# A failed run at level 0 must show level 0 + the failure pill — never a green/high level.
- out = dashboard.render_overview([_row(status="failure", level=0, has_screenshot=False,
- flags={}, level_cap_reason="L1 install FAILED")])
+ out = dashboard.render_overview(
+ [
+ _row(
+ status="failure",
+ level=0,
+ has_screenshot=False,
+ flags={},
+ level_cap_reason="L1 install FAILED",
+ )
+ ]
+ )
assert "level 0" in out
- assert dashboard.level_color(0) in out # red
+ assert dashboard.level_color(0) in out # red
assert dashboard._COLORS["failure"] in out
assert "level 4" not in out and "level 5" not in out and "level 6" not in out
- assert "no screenshot" in out # placeholder, no broken image
+ assert "no screenshot" in out # placeholder, no broken image
def test_level_pill_unknown_when_no_results():
@@ -74,7 +89,7 @@ def test_history_table_lists_runs():
assert "#4" in out and "#3" in out
assert "L4" in out and "L2" in out
assert "← all recipes" in out
- assert "/runs/4/summary.png" in out # per-run card link
+ assert "/runs/4/summary.png" in out # per-run card link
def test_history_empty():
@@ -83,12 +98,24 @@ def test_history_empty():
def test_build_row_projects_results(monkeypatch):
- monkeypatch.setattr(dashboard, "_results_for", lambda n: {
- "version": "1.2.3", "level": 2, "level_cap_reason": "cap",
- "screenshot": "screenshot.png", "flags": {"clean_teardown": True},
- })
- b = {"number": 7, "status": "success", "event": "custom",
- "params": {"RECIPE": "n8n", "REF": "abcdef1234567890"}, "finished": 10}
+ monkeypatch.setattr(
+ dashboard,
+ "_results_for",
+ lambda n: {
+ "version": "1.2.3",
+ "level": 2,
+ "level_cap_reason": "cap",
+ "screenshot": "screenshot.png",
+ "flags": {"clean_teardown": True},
+ },
+ )
+ b = {
+ "number": 7,
+ "status": "success",
+ "event": "custom",
+ "params": {"RECIPE": "n8n", "REF": "abcdef1234567890"},
+ "finished": 10,
+ }
r = dashboard._build_row(b)
assert r["recipe"] == "n8n" and r["number"] == 7
assert r["level"] == 2 and r["version"] == "1.2.3"
@@ -99,11 +126,16 @@ def test_build_row_projects_results(monkeypatch):
def test_build_row_degrades_without_results(monkeypatch):
# No results.json (e.g. an old run): grid still renders from Drone fields, level absent.
monkeypatch.setattr(dashboard, "_results_for", lambda n: {})
- b = {"number": 9, "status": "running", "event": "custom",
- "params": {"RECIPE": "ghost", "REF": "deadbeefcafe1234567890"}, "finished": 0}
+ b = {
+ "number": 9,
+ "status": "running",
+ "event": "custom",
+ "params": {"RECIPE": "ghost", "REF": "deadbeefcafe1234567890"},
+ "finished": 0,
+ }
r = dashboard._build_row(b)
assert r["level"] is None and r["has_screenshot"] is False
- assert r["version"] == "deadbeefcafe" # ref[:12] fallback
+ assert r["version"] == "deadbeefcafe" # ref[:12] fallback
# render must not crash or claim a level
assert "level —" in dashboard.render_overview([r])
@@ -111,7 +143,7 @@ def test_build_row_degrades_without_results(monkeypatch):
def test_level_badge_shows_level_coloured(monkeypatch):
svg = dashboard.render_level_badge("custom-html", 4)
assert "custom-html" in svg and "level 4" in svg
- assert dashboard.level_color(4) in svg # coloured by level
+ assert dashboard.level_color(4) in svg # coloured by level
assert svg.startswith("