-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add opt-in proxychains-style terminal chain visual #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import io | ||
| import json | ||
| import sys | ||
| import types | ||
|
|
@@ -65,5 +66,110 @@ def test_compute_readiness_relaxed_mode_requires_at_least_one_healthy_hop(self): | |
| self.assertEqual([], readiness["reasons"]) | ||
|
|
||
|
|
||
| class ChainVisualTests(unittest.TestCase): | ||
| def _cfg(self, hops=None, canary="exitserver:9999"): | ||
| return { | ||
| "chain": { | ||
| "hops": hops or [{"url": "http://proxy1:3128"}, {"url": "http://proxy2:3128"}], | ||
| "canary_target": canary, | ||
| }, | ||
| } | ||
|
|
||
| def test_topology_only_uses_pending_suffix(self): | ||
| """Without hop_statuses the visual ends with '...' to signal no probe yet.""" | ||
| visual = supervisor.format_chain_visual(self._cfg()) | ||
| self.assertIn("[egressd]", visual) | ||
| self.assertIn("|S-chain|", visual) | ||
| self.assertIn("...", visual) | ||
| self.assertNotIn("OK", visual) | ||
| self.assertNotIn("FAIL", visual) | ||
|
|
||
| def test_all_hops_ok_produces_ok_suffix(self): | ||
| """When all hops are healthy the final token is 'OK'.""" | ||
| statuses = { | ||
| "hop_0": {"ok": True, "elapsed_ms": 42}, | ||
| "hop_1": {"ok": True, "elapsed_ms": 38}, | ||
| } | ||
| visual = supervisor.format_chain_visual(self._cfg(), statuses) | ||
| self.assertIn("-<>-OK", visual) | ||
| self.assertNotIn("-XX-", visual) | ||
| self.assertNotIn("FAIL", visual) | ||
|
|
||
| def test_failed_hop_produces_fail_suffix_and_xx_connector(self): | ||
| """A failed hop uses '-XX-' connector and the line ends with 'FAIL'.""" | ||
| statuses = { | ||
| "hop_0": {"ok": True, "elapsed_ms": 42}, | ||
| "hop_1": {"ok": False, "error": "Connection refused"}, | ||
| } | ||
| visual = supervisor.format_chain_visual(self._cfg(), statuses) | ||
| self.assertIn("-XX-", visual) | ||
| self.assertIn("FAIL", visual) | ||
| self.assertNotIn("-<>-OK", visual) | ||
|
|
||
| def test_hop_labels_appear_in_chain_line(self): | ||
| """Each hop hostname:port must appear in the main chain line.""" | ||
| visual = supervisor.format_chain_visual(self._cfg()) | ||
| lines = visual.splitlines() | ||
| chain_line = lines[0] | ||
| self.assertIn("proxy1:3128", chain_line) | ||
| self.assertIn("proxy2:3128", chain_line) | ||
|
|
||
| def test_per_hop_detail_lines_present_when_statuses_provided(self): | ||
| """After the chain line there is one detail line per hop.""" | ||
| statuses = { | ||
| "hop_0": {"ok": True, "elapsed_ms": 42}, | ||
| "hop_1": {"ok": False, "error": "timeout"}, | ||
| } | ||
| visual = supervisor.format_chain_visual(self._cfg(), statuses) | ||
| lines = visual.splitlines() | ||
| # chain line + 2 detail lines | ||
| self.assertEqual(len(lines), 3) | ||
| self.assertIn("hop_0", lines[1]) | ||
| self.assertIn("hop_1", lines[2]) | ||
| self.assertIn("OK", lines[1]) | ||
| self.assertIn("FAIL", lines[2]) | ||
|
|
||
| def test_no_hops_returns_safe_message(self): | ||
| """An empty hops list must not raise; it returns a safe message.""" | ||
| cfg = {"chain": {"hops": [], "canary_target": "x:9"}} | ||
| visual = supervisor.format_chain_visual(cfg) | ||
| self.assertIn("no hops", visual) | ||
|
|
||
| def test_single_hop_chain(self): | ||
| """A single-hop chain produces exactly one hop label and no '-XX-'.""" | ||
| cfg = {"chain": {"hops": [{"url": "http://solo:3128"}], "canary_target": "t:80"}} | ||
| statuses = {"hop_0": {"ok": True, "elapsed_ms": 10}} | ||
| visual = supervisor.format_chain_visual(cfg, statuses) | ||
| self.assertIn("solo:3128", visual) | ||
| self.assertIn("-<>-OK", visual) | ||
| self.assertNotIn("-XX-", visual) | ||
|
|
||
| def _capture_stderr(self, fn, *args, **kwargs) -> str: | ||
| """Call *fn* with redirected stderr and return whatever was written.""" | ||
| buf = io.StringIO() | ||
| old_stderr = sys.stderr | ||
| try: | ||
| sys.stderr = buf | ||
| fn(*args, **kwargs) | ||
| finally: | ||
| sys.stderr = old_stderr | ||
| return buf.getvalue() | ||
|
Comment on lines
+147
to
+156
|
||
|
|
||
| def test_print_chain_visual_no_output_when_disabled(self): | ||
| """print_chain_visual must produce no output when chain_visual is false.""" | ||
| cfg = self._cfg() | ||
| cfg["logging"] = {"chain_visual": False} | ||
| output = self._capture_stderr(supervisor.print_chain_visual, cfg) | ||
| self.assertEqual(output, "") | ||
|
|
||
| def test_print_chain_visual_writes_to_stderr_when_enabled(self): | ||
| """print_chain_visual must write the visual to stderr when enabled.""" | ||
| cfg = self._cfg() | ||
| cfg["logging"] = {"chain_visual": True} | ||
| output = self._capture_stderr(supervisor.print_chain_visual, cfg) | ||
| self.assertIn("[egressd]", output) | ||
| self.assertIn("|S-chain|", output) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hop_health_looponly reprints when the aggregate_all_hops_ok(...)boolean flips, so per-hop transitions are suppressed whenever overall state stays unhealthy (for example, one failed hop is replaced by a different failed hop). This leaves operators with stale chain diagnostics despite real hop changes. Track and compare per-hop status (or anoktuple) rather than a single aggregate flag before deciding to print.Useful? React with 👍 / 👎.