返回 last30days-skill
test_competitors_plan_threading.py
根目录 / tests / test_competitors_plan_threading.py
1 """Tests for --competitors-plan JSON parsing and per-entity kwargs threading."""
2
3 from __future__ import annotations
4
5 import io
6 import json
7 import tempfile
8 import unittest
9 from contextlib import redirect_stderr
10 from pathlib import Path
11 from unittest import mock
12
13 import last30days as cli
14
15
16 class ParseCompetitorsPlanTests(unittest.TestCase):
17 def test_none_returns_empty(self):
18 self.assertEqual(cli.parse_competitors_plan(None), {})
19
20 def test_empty_string_returns_empty(self):
21 self.assertEqual(cli.parse_competitors_plan(""), {})
22
23 def test_inline_json_parsed(self):
24 raw = '{"Drake": {"x_handle": "Drake", "subreddits": ["Drizzy"]}}'
25 out = cli.parse_competitors_plan(raw)
26 self.assertIn("drake", out)
27 self.assertEqual(out["drake"]["x_handle"], "Drake")
28 self.assertEqual(out["drake"]["subreddits"], ["Drizzy"])
29
30 def test_file_path_accepted(self):
31 with tempfile.NamedTemporaryFile(
32 mode="w", suffix=".json", delete=False,
33 ) as f:
34 json.dump(
35 {"Anthropic": {"x_handle": "AnthropicAI", "github_user": "anthropics"}},
36 f,
37 )
38 path = f.name
39 try:
40 out = cli.parse_competitors_plan(path)
41 self.assertEqual(out["anthropic"]["x_handle"], "AnthropicAI")
42 self.assertEqual(out["anthropic"]["github_user"], "anthropics")
43 finally:
44 Path(path).unlink(missing_ok=True)
45
46 def test_file_with_non_ascii_content(self):
47 """Plan file with non-ASCII characters (e.g. accented names) reads without UnicodeDecodeError."""
48 with tempfile.NamedTemporaryFile(
49 mode="wb", suffix=".json", delete=False,
50 ) as f:
51 f.write(
52 b'{"Nestl\xc3\xa9": {"x_handle": "Nestle", "subreddits": ["nestle"]}}'
53 )
54 path = f.name
55 try:
56 out = cli.parse_competitors_plan(path)
57 self.assertIn("nestlé", out)
58 self.assertEqual(out["nestlé"]["x_handle"], "Nestle")
59 finally:
60 Path(path).unlink(missing_ok=True)
61
62 def test_case_insensitive_key_normalization(self):
63 raw = '{"DRAKE": {"x_handle": "Drake"}}'
64 out = cli.parse_competitors_plan(raw)
65 self.assertIn("drake", out)
66 self.assertNotIn("DRAKE", out)
67
68 def test_unknown_fields_warned_and_ignored(self):
69 raw = '{"Drake": {"x_handle": "Drake", "bogus_field": 42}}'
70 err = io.StringIO()
71 with redirect_stderr(err):
72 out = cli.parse_competitors_plan(raw)
73 self.assertIn("drake", out)
74 self.assertNotIn("bogus_field", out["drake"])
75 self.assertIn("Unknown fields", err.getvalue())
76
77 def test_malformed_json_exits_2(self):
78 with self.assertRaises(SystemExit) as cm, redirect_stderr(io.StringIO()) as err:
79 cli.parse_competitors_plan("{not valid json")
80 self.assertEqual(cm.exception.code, 2)
81 self.assertIn("Invalid JSON", err.getvalue())
82
83 def test_top_level_list_rejected(self):
84 with self.assertRaises(SystemExit) as cm, redirect_stderr(io.StringIO()):
85 cli.parse_competitors_plan('["Drake", "Kendrick"]')
86 self.assertEqual(cm.exception.code, 2)
87
88 def test_entry_non_dict_skipped_with_warning(self):
89 raw = '{"Drake": "not-a-dict", "Kendrick": {"x_handle": "kendricklamar"}}'
90 err = io.StringIO()
91 with redirect_stderr(err):
92 out = cli.parse_competitors_plan(raw)
93 self.assertNotIn("drake", out)
94 self.assertIn("kendrick", out)
95 self.assertIn("must be a dict", err.getvalue())
96
97 def test_all_six_fields_accepted(self):
98 raw = json.dumps({
99 "OpenAI": {
100 "x_handle": "OpenAI",
101 "x_related": ["sama", "gdb"],
102 "subreddits": ["OpenAI", "MachineLearning"],
103 "github_user": "openai",
104 "github_repos": ["openai/gpt-5"],
105 "context": "GPT-5 launch imminent",
106 }
107 })
108 out = cli.parse_competitors_plan(raw)
109 entry = out["openai"]
110 self.assertEqual(entry["x_handle"], "OpenAI")
111 self.assertEqual(entry["x_related"], ["sama", "gdb"])
112 self.assertEqual(entry["subreddits"], ["OpenAI", "MachineLearning"])
113 self.assertEqual(entry["github_user"], "openai")
114 self.assertEqual(entry["github_repos"], ["openai/gpt-5"])
115 self.assertEqual(entry["context"], "GPT-5 launch imminent")
116
117
118 class SubrunKwargsForTests(unittest.TestCase):
119 def test_plan_wins_over_auto_resolve(self):
120 plan_entry = {"x_handle": "Drake", "subreddits": ["Drizzy"]}
121 resolved = {"x_handle": "wrong", "subreddits": ["wrong"]}
122 kwargs = cli.subrun_kwargs_for("Drake", plan_entry, resolved=resolved)
123 self.assertEqual(kwargs["x_handle"], "Drake")
124 self.assertEqual(kwargs["subreddits"], ["Drizzy"])
125
126 def test_auto_resolve_used_when_plan_missing(self):
127 resolved = {
128 "x_handle": "Drake",
129 "subreddits": ["Drizzy", "hiphopheads"],
130 "github_user": "",
131 "github_repos": [],
132 }
133 kwargs = cli.subrun_kwargs_for("Drake", {}, resolved=resolved)
134 self.assertEqual(kwargs["x_handle"], "Drake")
135 self.assertEqual(kwargs["subreddits"], ["Drizzy", "hiphopheads"])
136
137 def test_both_empty_yields_all_none(self):
138 kwargs = cli.subrun_kwargs_for("Drake", {}, resolved={})
139 self.assertIsNone(kwargs["x_handle"])
140 self.assertIsNone(kwargs["subreddits"])
141 self.assertIsNone(kwargs["github_user"])
142 self.assertIsNone(kwargs["github_repos"])
143 self.assertIsNone(kwargs["x_related"])
144 self.assertEqual(kwargs["_context"], "")
145
146 def test_trustpilot_domain_plan_wins_and_is_not_hint(self):
147 kwargs = cli.subrun_kwargs_for(
148 "ThriftBooks",
149 {"trustpilot_domain": "www.thriftbooks.com"},
150 resolved={"trustpilot_domain": "wrong.com"},
151 )
152 self.assertEqual(kwargs["trustpilot_domain"], "www.thriftbooks.com")
153 self.assertFalse(kwargs["_trustpilot_domain_is_hint"])
154
155 def test_trustpilot_domain_from_auto_resolve_is_hint(self):
156 kwargs = cli.subrun_kwargs_for(
157 "ThriftBooks", {}, resolved={"trustpilot_domain": "thriftbooks.com"},
158 )
159 self.assertEqual(kwargs["trustpilot_domain"], "thriftbooks.com")
160 self.assertTrue(kwargs["_trustpilot_domain_is_hint"])
161
162 def test_trustpilot_domain_absent_is_none(self):
163 kwargs = cli.subrun_kwargs_for("ThriftBooks", {}, resolved={})
164 self.assertIsNone(kwargs["trustpilot_domain"])
165 self.assertFalse(kwargs["_trustpilot_domain_is_hint"])
166
167 def test_x_handle_strips_at_sign(self):
168 kwargs = cli.subrun_kwargs_for(
169 "Drake", {"x_handle": "@Drake"}, resolved={},
170 )
171 self.assertEqual(kwargs["x_handle"], "Drake")
172
173 def test_subreddits_strip_r_prefix(self):
174 kwargs = cli.subrun_kwargs_for(
175 "Drake", {"subreddits": ["r/Drizzy", "hiphopheads"]}, resolved={},
176 )
177 self.assertEqual(kwargs["subreddits"], ["Drizzy", "hiphopheads"])
178
179 def test_github_repos_filter_non_slash(self):
180 kwargs = cli.subrun_kwargs_for(
181 "Drake",
182 {"github_repos": ["drake/ovo", "not-a-repo"]},
183 resolved={},
184 )
185 self.assertEqual(kwargs["github_repos"], ["drake/ovo"])
186
187 def test_x_related_list_normalized(self):
188 kwargs = cli.subrun_kwargs_for(
189 "Drake",
190 {"x_related": ["@pnd", "drakefan"]},
191 resolved={},
192 )
193 self.assertEqual(kwargs["x_related"], ["pnd", "drakefan"])
194
195 def test_github_user_lowercased(self):
196 kwargs = cli.subrun_kwargs_for(
197 "OpenAI", {"github_user": "@OpenAI"}, resolved={},
198 )
199 self.assertEqual(kwargs["github_user"], "openai")
200
201 def test_context_from_plan_or_resolved(self):
202 plan_entry = {"context": "Plan context"}
203 resolved = {"context": "Resolved context"}
204 kwargs = cli.subrun_kwargs_for("X", plan_entry, resolved=resolved)
205 self.assertEqual(kwargs["_context"], "Plan context")
206
207 kwargs = cli.subrun_kwargs_for("X", {}, resolved=resolved)
208 self.assertEqual(kwargs["_context"], "Resolved context")
209
210 if __name__ == "__main__":
211 unittest.main()
212
212 lines PYTHON