Enhance path formatting in MultitestCommand with new grouping logic

- Updated the path formatting logic in `MultitestCommand` to support nested branches using shared prefixes and horizontal continuations.
- Introduced new utility functions for grouping and formatting suffix lines, improving the clarity of output paths.
- Modified the configuration example to clarify the behavior of the `condense_paths` option.
- Expanded test coverage to validate the new path formatting behavior, ensuring accurate representation of unique paths.
This commit is contained in:
agessaman
2026-03-29 11:02:30 -07:00
parent 9859b41d9e
commit 99e660afc3
3 changed files with 217 additions and 22 deletions
+1 -1
View File
@@ -974,7 +974,7 @@ enabled = false
# Leave empty to use default format
# Example: "Found {path_count} unique path(s) for @[{sender}]:\n{paths}"
response_format = @[{sender}] found {path_count} unique path(s):\n{paths}
# When true, {paths} uses shared-prefix lines plus tree branches (├ U+251C / └ U+2514) instead of repeating full paths
# When true, {paths} uses shared-prefix lines plus tree branches (├ U+251C / └ U+2514; continuations may use ├─ / └─ with U+2500)
condense_paths = false
[Greeter_Command]
+96 -17
View File
@@ -7,6 +7,7 @@ Listens for a period of time and collects all unique paths from incoming message
import asyncio
import re
import time
from collections import defaultdict
from dataclasses import dataclass
from typing import Optional
@@ -16,10 +17,14 @@ from .base_command import BaseCommand
_BRANCH_INTER = "\u251c" # ├ (intermediate branch)
_BRANCH_LAST = "\u2514" # └ (last branch)
_HORIZ = "\u2500" # ─ (BOX DRAWINGS LIGHT HORIZONTAL)
# Second-level branches: ├─ / └─ (horizontal continues the tee)
_BRANCH_CHILD_INTER = f"{_BRANCH_INTER}{_HORIZ} "
_BRANCH_CHILD_LAST = f"{_BRANCH_LAST}{_HORIZ} "
def _tree_branch_lines(suffixes: list[str]) -> list[str]:
"""Format branch rows: ├ for all but the last, └ for the last; space after the tree char."""
def _tree_branch_lines_flat(suffixes: list[str]) -> list[str]:
"""Format branch rows: ├ for all but the last, └ for the last; space after the tee only."""
if not suffixes:
return []
n = len(suffixes)
@@ -30,6 +35,61 @@ def _tree_branch_lines(suffixes: list[str]) -> list[str]:
return out
def _grouped_suffix_line_specs(non_empty: list[list[str]]) -> list[tuple[str, str]]:
"""Build (line_kind, text) rows: 'head' uses ├/└ + space; 'nest' uses ├─/└─ + text."""
by_first: dict[str, list[list[str]]] = defaultdict(list)
for suf in non_empty:
by_first[suf[0]].append(suf[1:])
specs: list[tuple[str, str]] = []
for ft in sorted(by_first.keys()):
rests = by_first[ft]
if len(rests) == 1:
r = rests[0]
if not r:
specs.append(("head", ft))
else:
specs.append(("head", ",".join([ft, *r])))
continue
specs.append(("head", ft))
for rem in sorted((r for r in rests if r), key=lambda x: ",".join(x)):
specs.append(("nest", ",".join(rem)))
return specs
def _apply_tee_prefixes(specs: list[tuple[str, str]]) -> list[str]:
"""Assign ├/└ and ├─/└─ from flattened order; only the final row uses └/└─."""
n = len(specs)
out: list[str] = []
for i, (kind, text) in enumerate(specs):
last = i == n - 1
if kind == "head":
p = _BRANCH_LAST if last else _BRANCH_INTER
out.append(f"{p} {text}")
else:
p = _BRANCH_CHILD_LAST if last else _BRANCH_CHILD_INTER
out.append(f"{p}{text}")
return out
def _format_suffix_branch_lines(suffix_tokens: list[list[str]], short_ellipsis: bool) -> list[str]:
"""Format suffixes after display LCP: group by first hop, nest continuations with U+2500."""
non_empty = [s for s in suffix_tokens if s]
if not non_empty:
return _tree_branch_lines_flat(["..."]) if short_ellipsis else []
if len(non_empty) == 1:
parts = [",".join(non_empty[0])]
if short_ellipsis:
parts.append("...")
return _tree_branch_lines_flat(parts)
specs = _grouped_suffix_line_specs(non_empty)
if short_ellipsis:
specs.append(("head", "..."))
return _apply_tee_prefixes(specs)
def _path_to_tokens(path: str) -> list[str]:
"""Split a comma-separated path into non-empty hex token strings."""
return [p.strip() for p in path.split(",") if p.strip()]
@@ -62,8 +122,32 @@ def _longest_common_prefix(token_lists: list[list[str]]) -> list[str]:
return list(first[: min(len(tl) for tl in token_lists)])
def _shrink_display_lcp(maximal: list[list[str]], lcp: list[str]) -> list[str]:
"""Shorten displayed LCP when one path ends exactly at LCP and another continues past it.
Avoids showing the shorter path as the 'trunk' with only the tail as a branch (e.g. …0101
on the common line and └ 0970), which reads like a single endpoint plus an offshoot.
"""
lcp = list(lcp)
while len(lcp) > 1:
has_exact = any(t == lcp for t in maximal)
has_extend = any(_is_strict_prefix(lcp, t) for t in maximal)
if has_exact and has_extend:
lcp.pop()
else:
break
return lcp
def _format_path_cluster(token_lists: list[list[str]], use_brackets: bool) -> list[str]:
"""Format a cluster of token lists into condensed lines (common prefix + ├/└ branches)."""
"""Format a cluster into condensed lines (common prefix + ├/└ and ├─/└─).
Suffixes are grouped by their first hop after the display LCP; multiple variants under the same
hop use one ├ line for that hop and ├─/└─ for continuations. The last line of the block uses └/└─.
If one path stops exactly where another continues, the displayed LCP is shortened so the shared
segment is not mistaken for a single endpoint (e.g. only └ tail after a full shorter path).
"""
token_lists = [t for t in token_lists if t]
if not token_lists:
return []
@@ -75,29 +159,24 @@ def _format_path_cluster(token_lists: list[list[str]], use_brackets: bool) -> li
short_ellipsis = len(maximal) < len(token_lists)
if not maximal:
return _tree_branch_lines(["..."]) if short_ellipsis else []
return _tree_branch_lines_flat(["..."]) if short_ellipsis else []
lcp = _longest_common_prefix(maximal)
raw_lcp = _longest_common_prefix(maximal)
lcp = _shrink_display_lcp(maximal, raw_lcp)
if len(lcp) > 0:
suffix_tokens = [t[len(lcp) :] for t in maximal]
lines = [",".join(lcp)]
branches: list[str] = []
for t in maximal:
suf = t[len(lcp) :]
if suf:
branches.append(",".join(suf))
suffix_parts = sorted(branches)
if short_ellipsis:
suffix_parts.append("...")
if suffix_parts:
lines.extend(_tree_branch_lines(suffix_parts))
branch_lines = _format_suffix_branch_lines(suffix_tokens, short_ellipsis)
if branch_lines:
lines.extend(branch_lines)
return lines
if len(maximal) == 1:
s = ",".join(maximal[0])
lines = [f"[{s}]"] if use_brackets else [s]
if short_ellipsis:
lines.extend(_tree_branch_lines(["..."]))
lines.extend(_tree_branch_lines_flat(["..."]))
return lines
groups: dict[str, list[list[str]]] = {}
@@ -110,7 +189,7 @@ def _format_path_cluster(token_lists: list[list[str]], use_brackets: bool) -> li
sub_lines = _format_path_cluster(groups[ft], use_brackets=multi)
lines.extend(sub_lines)
if short_ellipsis:
lines.extend(_tree_branch_lines(["..."]))
lines.extend(_tree_branch_lines_flat(["..."]))
return lines
+120 -4
View File
@@ -12,6 +12,9 @@ from tests.conftest import mock_message
_INTER = "\u251c"
_LAST = "\u2514"
_HORIZ = "\u2500"
_CHILD_INTER = f"{_INTER}{_HORIZ} "
_CHILD_LAST = f"{_LAST}{_HORIZ} "
def _make_bot():
@@ -162,7 +165,7 @@ class TestCondensePathLines:
"e6,0c,85,82,28,1a,cd,7e",
f"{_INTER} 01",
f"{_INTER} 7a",
f"{_INTER} 7a,09",
f"{_CHILD_INTER}09",
f"{_LAST} ...",
]
)
@@ -177,11 +180,124 @@ class TestCondensePathLines:
]
)
out = _condense_path_lines(paths)
# LCP shrinks so cc is not the whole “trunk” while dd/ee branch off
expected = "\n".join(
[
"aa,bb,cc",
f"{_INTER} dd",
f"{_LAST} ee",
"aa,bb",
f"{_INTER} cc",
f"{_CHILD_INTER}dd",
f"{_CHILD_LAST}ee",
]
)
assert out == expected
def test_one_path_ends_at_lcp_other_extends(self):
"""Shorter LCP so both 0101 and 0101,0970 appear as branches, not one hidden on the trunk."""
paths = sorted(["cdf1,7e76,0101", "cdf1,7e76,0101,0970"])
out = _condense_path_lines(paths)
expected = "\n".join(
[
"cdf1,7e76",
f"{_INTER} 0101",
f"{_CHILD_LAST}0970",
]
)
assert out == expected
def test_overlapping_suffix_branches_under_common_prefix(self):
paths = sorted(
[
"cdf119,860cca,010101",
"cdf119,860cca,e0eed9",
"cdf119,860cca,e0eed9,1ed612",
]
)
out = _condense_path_lines(paths)
expected = "\n".join(
[
"cdf119,860cca",
f"{_INTER} 010101",
f"{_INTER} e0eed9",
f"{_CHILD_LAST}1ed612",
]
)
assert out == expected
def test_divergent_routes_with_shared_mid_prefix(self):
"""TRM-style: group by first hop (13) so 01 vs 01,1e nest under ├─."""
paths = sorted(["41,96,13,01", "41,96,13,01,1e", "41,96,83,09"])
out = _condense_path_lines(paths)
expected = "\n".join(
[
"41,96",
f"{_INTER} 13",
f"{_CHILD_INTER}01",
f"{_CHILD_INTER}01,1e",
f"{_LAST} 83,09",
]
)
assert out == expected
def test_mixed_first_hops_nest_per_group(self):
"""Ill Eagle-style: 01 vs 01,1e share a group; 09 and e0 are separate top-level branches."""
paths = sorted(
[
"e2,ab,1f,ef,55,21,01",
"e2,ab,1f,ef,55,21,01,1e",
"e2,ab,1f,ef,55,21,09",
"e2,ab,1f,ef,55,21,e0",
]
)
out = _condense_path_lines(paths)
expected = "\n".join(
[
"e2,ab,1f,ef,55,21",
f"{_INTER} 01",
f"{_CHILD_INTER}1e",
f"{_INTER} 09",
f"{_LAST} e0",
]
)
assert out == expected
def test_shorter_path_one_extra_hop_still_trees(self):
"""860cca vs 860cca,010101: shrink trunk so both show as branches."""
paths = sorted(
[
"d38a05,c4a86a,067b75,cafee0,1ffbd6,e8154b,860cca,010101",
"d38a05,c4a86a,067b75,cafee0,1ffbd6,e8154b,860cca",
]
)
out = _condense_path_lines(paths)
expected = "\n".join(
[
"d38a05,c4a86a,067b75,cafee0,1ffbd6,e8154b",
f"{_INTER} 860cca",
f"{_CHILD_LAST}010101",
]
)
assert out == expected
def test_shared_hop_then_horiz_continuations(self):
"""All paths share first hop after LCP → one ├ hop line then ├─/└─ remainders (U+2500)."""
paths = sorted(
[
"d38a05,479198,a837bc,7e7662,e0eed9",
"d38a05,479198,a837bc,7e7662,e0eed9,010101",
"d38a05,479198,a837bc,7e7662,e0eed9,0970d6",
"d38a05,479198,a837bc,7e7662,e0eed9,1ed612",
"d38a05,479198,a837bc,7e7662,e0eed9,f",
]
)
out = _condense_path_lines(paths)
expected = "\n".join(
[
"d38a05,479198,a837bc,7e7662",
f"{_INTER} e0eed9",
f"{_CHILD_INTER}010101",
f"{_CHILD_INTER}0970d6",
f"{_CHILD_INTER}1ed612",
f"{_CHILD_LAST}f",
]
)
assert out == expected