返回 last30days-skill
test_competitors_routing.py
根目录 / tests / test_competitors_routing.py
1 """Routing for vs-mode / --competitors / --competitors-plan (#868).
2
3 Plan implies competitor mode; vs-split supplies peers when not in discover-N
4 mode; entity caps align with COMPETITORS_MAX and warn when truncating.
5 """
6
7 from __future__ import annotations
8
9 import io
10 import json
11 import unittest
12 from contextlib import redirect_stderr
13
14 import last30days as cli
15 from lib import competitors, planner, render
16
17
18 def _parse(*argv: str):
19 return cli.build_parser().parse_args(argv)
20
21
22 def _route(topic: str, *argv: str):
23 args = _parse(topic, *argv)
24 enabled, count, explicit = cli.resolve_competitors_args(args)
25 plan = cli.parse_competitors_plan(args.competitors_plan)
26 return cli.apply_vs_competitor_routing(
27 topic,
28 competitors_flag=args.competitors,
29 comp_enabled=enabled,
30 comp_count=count,
31 comp_explicit=explicit,
32 comp_plan=plan,
33 )
34
35
36 FIVE_WAY = "Weber vs Traeger vs Big Green Egg vs Blackstone vs Napoleon Grills"
37
38
39 class TestCompetitorsPlanImpliesEnabled(unittest.TestCase):
40 def test_plan_alone_enables_competitor_mode(self):
41 plan = json.dumps({
42 "Traeger": {"x_handle": "TraegerGrills"},
43 "Napoleon Grills": {"subreddits": ["NapoleonGrill"]},
44 })
45 args = _parse("Weber grills", "--competitors-plan", plan)
46 enabled, count, explicit = cli.resolve_competitors_args(args)
47 self.assertTrue(enabled)
48 self.assertEqual(explicit, []) # peers filled by routing from plan keys
49 topic, enabled, count, explicit = _route(
50 "Weber grills", "--competitors-plan", plan,
51 )
52 self.assertEqual(topic, "Weber grills")
53 self.assertTrue(enabled)
54 self.assertEqual(explicit, ["Traeger", "Napoleon Grills"])
55 self.assertEqual(count, 2)
56
57 def test_plan_does_not_override_explicit_list(self):
58 plan = json.dumps({"Ignored": {}})
59 topic, enabled, count, explicit = _route(
60 "Weber",
61 "--competitors-list", "Traeger,Blackstone",
62 "--competitors-plan", plan,
63 )
64 self.assertEqual(topic, "Weber")
65 self.assertTrue(enabled)
66 self.assertEqual(explicit, ["Traeger", "Blackstone"])
67 self.assertEqual(count, 2)
68
69
70 class TestVsRoutingWithPlan(unittest.TestCase):
71 def test_five_way_vs_plus_plan_keeps_all_five(self):
72 plan = json.dumps({
73 "Traeger": {},
74 "Big Green Egg": {},
75 "Blackstone": {},
76 "Napoleon Grills": {"subreddits": ["NapoleonGrill"]},
77 })
78 err = io.StringIO()
79 with redirect_stderr(err):
80 topic, enabled, count, explicit = _route(
81 FIVE_WAY, "--competitors-plan", plan,
82 )
83 self.assertEqual(topic, "Weber")
84 self.assertTrue(enabled)
85 self.assertEqual(
86 explicit,
87 ["Traeger", "Big Green Egg", "Blackstone", "Napoleon Grills"],
88 )
89 self.assertEqual(count, 4)
90 self.assertIn("Napoleon Grills", err.getvalue())
91
92 def test_bare_five_way_vs_keeps_all_five(self):
93 with redirect_stderr(io.StringIO()):
94 topic, enabled, count, explicit = _route(FIVE_WAY)
95 self.assertEqual(topic, "Weber")
96 self.assertEqual(
97 explicit,
98 ["Traeger", "Big Green Egg", "Blackstone", "Napoleon Grills"],
99 )
100
101 def test_competitors_n_on_vs_topic_skips_vs_split(self):
102 """Discover-N mode must not rewrite a vs-string into named peers."""
103 topic, enabled, count, explicit = _route(FIVE_WAY, "--competitors", "2")
104 self.assertEqual(topic, FIVE_WAY)
105 self.assertTrue(enabled)
106 self.assertEqual(count, 2)
107 self.assertEqual(explicit, [])
108
109
110 class TestEntityCapAlignment(unittest.TestCase):
111 def test_comparison_entity_max_matches_competitors_max_plus_main(self):
112 self.assertEqual(
113 competitors.COMPARISON_ENTITY_MAX,
114 competitors.COMPETITORS_MAX + 1,
115 )
116 self.assertEqual(
117 planner._max_subqueries("comparison"),
118 competitors.COMPARISON_ENTITY_MAX + 1, # primary + per-entity
119 )
120
121 def test_five_way_not_silently_truncated(self):
122 entities = planner._comparison_entities(FIVE_WAY)
123 self.assertEqual(
124 entities,
125 ["Weber", "Traeger", "Big Green Egg", "Blackstone", "Napoleon Grills"],
126 )
127
128 def test_fallback_plan_keeps_all_entities_at_ceiling(self):
129 topic = "A vs B vs C vs D vs E vs F vs G"
130 entities = planner._comparison_entities(topic)
131 self.assertEqual(len(entities), competitors.COMPARISON_ENTITY_MAX)
132 plan = planner._fallback_plan(topic, ["reddit"], None, "standard")
133 entity_labels = [sq.label for sq in plan.subqueries if sq.label.startswith("entity-")]
134 self.assertEqual(len(entity_labels), competitors.COMPARISON_ENTITY_MAX)
135 self.assertIn("primary", [sq.label for sq in plan.subqueries])
136
137 def test_empty_plan_alone_errors_clearly(self):
138 args = _parse("Weber grills", "--competitors-plan", "{}")
139 enabled, count, explicit = cli.resolve_competitors_args(args)
140 plan = cli.parse_competitors_plan(args.competitors_plan)
141 topic, enabled, count, explicit = cli.apply_vs_competitor_routing(
142 "Weber grills",
143 competitors_flag=args.competitors,
144 comp_enabled=enabled,
145 comp_count=count,
146 comp_explicit=explicit,
147 comp_plan=plan,
148 )
149 self.assertTrue(enabled)
150 self.assertEqual(explicit, [])
151 # Guard in _main: plan present, no peers, not discover-N
152 self.assertIsNone(args.competitors)
153 self.assertTrue(args.competitors_plan)
154 self.assertFalse(explicit)
155
156 def test_over_max_warns_and_names_dropped(self):
157 # main + 6 peers = max; 8th entity dropped with warning
158 topic = "A vs B vs C vs D vs E vs F vs G vs H"
159 err = io.StringIO()
160 with redirect_stderr(err):
161 kept = cli.truncate_comparison_entities(
162 planner._comparison_entities(topic, uncapped=True),
163 warn=True,
164 )
165 self.assertEqual(len(kept), competitors.COMPARISON_ENTITY_MAX)
166 self.assertIn("dropped", err.getvalue().lower())
167 self.assertIn("H", err.getvalue())
168
169 def test_render_scaffold_keeps_five_columns(self):
170 entities = render._parse_comparison_entities(FIVE_WAY)
171 self.assertEqual(len(entities), 5)
172 lines = render._render_comparison_scaffold(FIVE_WAY)
173 header = next(line for line in lines if line.startswith("| Dimension |"))
174 self.assertIn("Napoleon Grills", header)
175
176
177 if __name__ == "__main__":
178 unittest.main()
179
179 lines PYTHON