#!/usr/bin/env bats # Seam: the service inventory (rank 3). shared/services.yml is the one # inventory of deployable services; scripts/service-map.py is its only # reader, and every consumer (deploy-plan.py, deploy-remote.sh's # allow-list, deploy.sh's --service gate, test-changed.sh's seam mapper, # the Pi-hole hosts render, the Prometheus scrape facts, the homepage # probes) goes through it. These tests pin the inventory's integrity, # the planner's selection against a captured fixture, and the rendered # artifacts' drift against the tracked copies. load helpers @test "inventory: schema, unit claims, path claims, compose ownership, and render drift are sound" { run python3 "$REPO_ROOT/scripts/service-map.py" check [ "$status" -eq 0 ] } @test "inventory: the deployers' allow-list is the inventory" { # Every service the inventory places on a host must pass "allows" for # that host, and a name the inventory never places must be refused. for host in dankclaw dankserver frankbook-pro; do while IFS= read -r svc; do [ -n "$svc" ] || continue run python3 "$REPO_ROOT/scripts/service-map.py" allows --host "$host" --service "$svc" [ "$status" -eq 0 ] || { echo "allows refused $host:$svc"; return 1; } done < <(python3 "$REPO_ROOT/scripts/service-map.py" services --host "$host") done run python3 "$REPO_ROOT/scripts/service-map.py" allows --host dankserver --service sonarr [ "$status" -ne 0 ] # the *arr stack lives on frankbook-pro, not dankserver run python3 "$REPO_ROOT/scripts/service-map.py" allows --host dankclaw --service frank-archon [ "$status" -ne 0 ] # frank-archon lives on dankserver } @test "inventory: the planner reproduces the pinned selection fixture" { # tests/fixtures/service-plan-golden.tsv is the characterization of # path -> service selection across every tracked path, every target, # and the multi-path combos the old hand-written planner was fed. python3 - "$REPO_ROOT" <<'PYEOF' import importlib.util import sys root = sys.argv[1] spec = importlib.util.spec_from_file_location("sm", root + "/scripts/service-map.py") sm = importlib.util.module_from_spec(spec) spec.loader.exec_module(sm) inv = sm.Inventory.load(root + "/shared/services.yml") bad = [] with open(root + "/tests/fixtures/service-plan-golden.tsv") as fh: for line in fh: host, paths, want = line.rstrip("\n").split("\t") got = ",".join(sm.select(inv, host, paths.split("+"))) if got != want: bad.append(f"{host} {paths}\n want: {want}\n got: {got}") if bad: print(f"{len(bad)} selection mismatches:", *bad[:20], sep="\n") sys.exit(1) PYEOF } @test "inventory: deploy-plan.py keeps its CLI contract on top of service-map" { # stdin paths, positional paths, and the --repo/--base/--head diff mode # all route to the same selection. run bash -c "cd '$REPO_ROOT' && printf '%s\n' dankclaw/searxng/quadlet/gluetun.container | \ scripts/deploy-plan.py --target dankclaw" [ "$status" -eq 0 ] [ "${lines[0]}" = "gluetun" ] run "$REPO_ROOT/scripts/deploy-plan.py" --target dankserver scripts/frank-archon.env [ "$status" -eq 0 ] [ "${lines[0]}" = "frank-archon" ] run "$REPO_ROOT/scripts/deploy-plan.py" --target dankclaw --repo "$REPO_ROOT" --base HEAD --head HEAD [ "$status" -eq 0 ] [ -z "$output" ] } @test "inventory: the Pi-hole hosts file is a rendered artifact" { # The tracked addn-hosts must be byte-identical to the render; editing # it by hand is drift the inventory check also catches. run bash -c "cd '$REPO_ROOT' && python3 scripts/service-map.py render pihole-hosts | \ diff -u dankclaw/pihole/addn-hosts -" [ "$status" -eq 0 ] } @test "inventory: changed-test seams for a service unit are the service's own" { # The rank-3 contract: a unit change selects the OWNING service's # declared seams, not the old shotgun of unrelated quadlet seams. run bash "$REPO_ROOT/scripts/test-changed.sh" --list --paths dankclaw/gitea/quadlet/gitea.network [ "$status" -eq 0 ] [[ "$output" == *"tests/caddyfile.bats"* ]] [[ "$output" == *"tests/deployment_scoping.bats"* ]] [[ "$output" != *"tests/open_webui_quadlet.bats"* ]] [[ "$output" != *"tests/searxng_quadlet.bats"* ]] } @test "inventory: every tracked quadlet unit is claimed by exactly one service" { # check units covers claim conflicts; this pins the census size so a # unit added WITHOUT an inventory entry (unclaimed) fails the seam. python3 - "$REPO_ROOT" <<'PYEOF' import importlib.util import subprocess import sys from pathlib import PurePosixPath root = sys.argv[1] spec = importlib.util.spec_from_file_location("sm", root + "/scripts/service-map.py") sm = importlib.util.module_from_spec(spec) spec.loader.exec_module(sm) inv = sm.Inventory.load(root + "/shared/services.yml") graph = sm.graph_for(tuple(inv.hosts)) owned = {u for u in graph.units} problems = [] tracked_units = [ p for p in sm.tracked_files() if PurePosixPath(p).name.endswith((".container", ".network")) and not sm.is_trash(p) ] for path in tracked_units: parts = PurePosixPath(path).parts name = parts[-1] key = (parts[0], name.rsplit(".", 1)[0]) if key not in graph.units and key not in graph.networks: problems.append(f"unclaimed unit-file path: {path}") if problems: print("\n".join(problems)) sys.exit(1) PYEOF }