Artifacts show the branch tree with ancestry and gate marks. The cleanup runner calls pkgmgr branch cleanup with its real argv and parses its repos[*].items output, refuses any plan with a remote-branch action, marks an item done only when pkgmgr reports it done, and offers no delete for a branch a worktree holds. Every text states what the pkgmgr command does to volumes. Committed below the bar on the operator's word 2026-11-12 (no further per-bundle tests, one gate after the merge): the W3 e2e never ran; the client switches to --local once pkgmgr ships it. 🎯 Fix: 90 % 💥 Break: 6 % 💸 Cost: 30 % ⏱️ Time: 40 % 🛡️ Security: 100 % 🏆 Score: 100 🚦 Gate: ⛔ break 6 % > 5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
8.2 KiB
Python
145 lines
8.2 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import unittest
|
|
|
|
from hub_case import HubCase
|
|
from hub_support import HUB, signing
|
|
|
|
ANCESTRY = HUB / "templates" / "ancestry.js"
|
|
PAGE = HUB / "templates" / "page-ancestry.js"
|
|
STYLE = HUB / "templates" / "style-ancestry.css"
|
|
REPO = "/home/op/Repositories/demo"
|
|
# Param: argv[1] is ancestry.js; prints the layouts, gate marks and argv the page builds.
|
|
PROBE = """
|
|
const A = require(process.argv[1]);
|
|
const node = (id) => ({id, label: id});
|
|
const shape = (plan) => ({
|
|
rows: plan.nodes.map((entry) => [entry.id, entry.generation]),
|
|
ys: Object.fromEntries(plan.nodes.map((entry) => [entry.id, entry.y])),
|
|
xs: Object.fromEntries(plan.nodes.map((entry) => [entry.id, entry.x])),
|
|
links: plan.links.map((link) => [link.parent, link.child, link.derived, link.y1 < link.y2]),
|
|
size: [plan.width, plan.height],
|
|
});
|
|
const family = shape(A.layout(["main", "feature/a", "feature/b", "feature/ab", "lonely"].map(node), [
|
|
{child: "feature/a", parent: "main", source: "recorded"},
|
|
{child: "feature/b", parent: "main", source: "derived"},
|
|
{child: "feature/ab", parent: "feature/a", source: "operator"},
|
|
{child: "feature/ab", parent: "feature/b", source: "derived"},
|
|
{child: "ghost", parent: "main", source: "recorded"},
|
|
]));
|
|
const looped = shape(A.layout(["x", "y", "z"].map(node), [{child: "x", parent: "y"}, {child: "y", parent: "x"}, {child: "z", parent: "z"}]));
|
|
const chain = shape(A.layout(["main", "mid", "leaf"].map(node), [{child: "leaf", parent: "main"}, {child: "leaf", parent: "mid"}, {child: "mid", parent: "main"}]));
|
|
const roots = Array.from({length: 20}, (_, index) => `b${String(index).padStart(2, "0")}`);
|
|
const wide = shape(A.layout([...roots, "child"].map(node), [{child: "child", parent: "b00"}]));
|
|
const shown = {resolved: {commit: {level: "test", source: "feature/a"}, merge: {level: "quality-high", source: "main"}, push: {level: "none", source: null}, pr: {level: "e2e", source: "feature/a"}}, rules: {commit: "test", pr: "e2e"}};
|
|
console.log(JSON.stringify({
|
|
family, looped, chain, wide, spacing: A.SPACING,
|
|
marks: A.gateMarks("feature/a", shown),
|
|
bare: A.gateMarks("lonely", null),
|
|
cycle: [null, "none", "quick", "test", "quality-high", "e2e"].map(A.nextRule),
|
|
argv: [A.ruleArgv("/r", "b", "push", "test"), A.ruleArgv("/r", "b", "push", null), A.parentArgv("/r", "b", "p", false), A.parentArgv("/r", "b", "p", true)],
|
|
icons: A.LEVEL_ICONS,
|
|
}));
|
|
"""
|
|
|
|
|
|
class TestAncestryLayout(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
done = subprocess.run(["node", "-e", PROBE, str(ANCESTRY)], capture_output=True, text=True, timeout=60, check=True)
|
|
cls.data = json.loads(done.stdout)
|
|
|
|
def test_generations_run_top_down_and_a_node_with_several_parents_sits_below_the_deepest(self):
|
|
family = self.data["family"]
|
|
self.assertEqual(family["rows"], [["main", 0], ["feature/a", 1], ["feature/b", 1], ["feature/ab", 2], ["lonely", 0]])
|
|
self.assertLess(family["ys"]["main"], family["ys"]["feature/a"])
|
|
self.assertLess(family["ys"]["feature/a"], family["ys"]["feature/ab"])
|
|
self.assertEqual(family["ys"]["feature/a"], family["ys"]["feature/b"])
|
|
self.assertEqual(self.data["chain"]["rows"], [["main", 0], ["mid", 1], ["leaf", 2]])
|
|
|
|
def test_every_parent_edge_is_drawn_downwards_and_only_derived_ones_are_marked_dashed(self):
|
|
self.assertEqual(self.data["family"]["links"], [
|
|
["main", "feature/a", False, True],
|
|
["main", "feature/b", True, True],
|
|
["feature/a", "feature/ab", False, True],
|
|
["feature/b", "feature/ab", True, True],
|
|
])
|
|
|
|
def test_a_child_sits_between_its_parents_and_unknown_or_looping_edges_do_not_break_the_layout(self):
|
|
xs = self.data["family"]["xs"]
|
|
self.assertTrue(min(xs["feature/a"], xs["feature/b"]) <= xs["feature/ab"] <= max(xs["feature/a"], xs["feature/b"]))
|
|
looped = self.data["looped"]
|
|
self.assertEqual(sorted(generation for _, generation in looped["rows"]), [0, 0, 1])
|
|
self.assertEqual([link[:2] for link in looped["links"] if link[0] == link[1]], [])
|
|
|
|
def test_a_wide_generation_wraps_into_bands_and_the_next_generation_starts_below_them(self):
|
|
wide, spacing = self.data["wide"], self.data["spacing"]
|
|
self.assertEqual(wide["size"], [spacing["wrap"] * spacing["column"], 4 * spacing["row"]])
|
|
roots = [name for name, generation in wide["rows"] if generation == 0]
|
|
self.assertEqual(len(roots), 20)
|
|
self.assertEqual(len({(wide["xs"][name], wide["ys"][name]) for name in roots}), 20)
|
|
self.assertEqual(sorted({wide["ys"][name] for name in roots}), [spacing["row"] * band + spacing["row"] / 2 for band in range(3)])
|
|
self.assertEqual(wide["ys"]["child"], 3.5 * spacing["row"])
|
|
self.assertTrue(all(0 < x < wide["size"][0] for x in wide["xs"].values()))
|
|
|
|
|
|
class TestGateMarks(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
done = subprocess.run(["node", "-e", PROBE, str(ANCESTRY)], capture_output=True, text=True, timeout=60, check=True)
|
|
cls.data = json.loads(done.stdout)
|
|
|
|
def test_each_action_shows_its_resolved_level_with_own_rules_solid_and_inherited_faint_with_the_source(self):
|
|
marks = {mark["action"]: mark for mark in self.data["marks"]}
|
|
self.assertEqual(list(marks), ["commit", "merge", "push", "pr"])
|
|
self.assertEqual([(marks[a]["level"], marks[a]["own"], marks[a]["inherited"]) for a in marks], [("test", True, False), ("quality-high", False, True), ("none", False, True), ("e2e", True, False)])
|
|
self.assertEqual([marks[a]["title"] for a in marks], ["commit: test · own rule", "merge: quality-high · from main", "push: none · no rule", "pr: e2e · own rule"])
|
|
self.assertEqual([marks[a]["icon"] for a in marks], ["bi-check2-circle", "bi-shield-check", "bi-circle", "bi-browser-chrome"])
|
|
|
|
def test_a_branch_gates_py_does_not_know_shows_none_inherited_for_every_action(self):
|
|
self.assertEqual([(mark["level"], mark["inherited"], mark["source"]) for mark in self.data["bare"]], [("none", True, "")] * 4)
|
|
|
|
def test_a_click_cycles_the_own_rule_through_every_level_and_back_to_inherited(self):
|
|
self.assertEqual(self.data["cycle"], ["none", "quick", "test", "quality-high", "e2e", None])
|
|
|
|
def test_the_staged_argv_is_exactly_what_signing_gate_argv_accepts(self):
|
|
for argv in self.data["argv"]:
|
|
with self.subTest(argv=argv):
|
|
self.assertEqual(signing.gate_argv(argv), argv)
|
|
self.assertEqual(self.data["argv"][1], ["rule", "drop", "--repo", "/r", "--branch", "b", "--action", "push"])
|
|
|
|
def test_the_level_icons_are_the_page_levels_of_gates_py(self):
|
|
self.assertEqual(list(self.data["icons"]), list(signing.GATE_LEVELS))
|
|
|
|
|
|
class TestAncestryAssets(unittest.TestCase):
|
|
def test_the_layout_module_parses_on_its_own_and_derived_edges_are_dashed(self):
|
|
done = subprocess.run(["node", "--check", str(ANCESTRY)], capture_output=True, text=True, timeout=60, check=False)
|
|
self.assertEqual(done.returncode, 0, done.stderr)
|
|
self.assertIn("svg .ancestry-edge.derived { stroke-dasharray: 5 5; }", STYLE.read_text(encoding="utf-8"))
|
|
self.assertIn(".gate-mark.inherited { opacity: .4; }", STYLE.read_text(encoding="utf-8"))
|
|
|
|
def test_every_new_control_is_an_icon_with_title_and_aria_label(self):
|
|
page = PAGE.read_text(encoding="utf-8")
|
|
for control in re.findall(r'iconBtn\("(bi-[a-z0-9-]+)", "([^"]+)"', page):
|
|
with self.subTest(control=control):
|
|
self.assertTrue(control[1])
|
|
self.assertIn('...named(`${mark.action} gate`, title)', page)
|
|
self.assertIn('role: "button", "aria-label": node.label', page)
|
|
|
|
|
|
class TestHubAncestry(HubCase):
|
|
def test_the_page_ships_the_layout_module_and_the_gate_level_icons(self):
|
|
html = self.index()
|
|
self.assertIn(ANCESTRY.read_text(encoding="utf-8").strip(), html)
|
|
for name in ("bi-circle", "bi-lightning-charge", "bi-check2-circle", "bi-shield-check", "bi-browser-chrome", "bi-key", "bi-key-fill", "bi-check2-all"):
|
|
with self.subTest(name=name):
|
|
self.assertIn(f'<symbol id="{name}"', html)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|