fuzzyworld

fuzzyworld-tools / files / tests / test_gen.py

test_gen.py

Blame: the commit that last changed each line · file · history

b447a9f2 2026-11-03 Pavel Safronov1"""Tests for the page furniture in server/gen.py. Run: python3 -m unittest discover tests"""
799905ae 2026-11-04 Pavel Safronov2import datetime
3import json
b447a9f2 2026-11-03 Pavel Safronov4import os
799905ae 2026-11-04 Pavel Safronov5import re
6import shutil
7import subprocess
b447a9f2 2026-11-03 Pavel Safronov8import sys
799905ae 2026-11-04 Pavel Safronov9import tempfile
b447a9f2 2026-11-03 Pavel Safronov10import unittest
ced0eafa 2026-11-04 Pavel Safronov11from unittest import mock
b447a9f2 2026-11-03 Pavel Safronov12
13sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "server"))
14import gen # noqa: E402
15
16
17class ReloadTimer(unittest.TestCase):
18 def test_refreshing_page_has_meta_refresh_bar_and_countdown(self):
19 html = gen.page("T", '<p>x <span class="reload-in"></span></p>', refresh=15)
20 self.assertIn('<meta http-equiv="refresh" content="15">', html)
21 self.assertIn('class="reload-bar"', html)
22 self.assertIn("animation: reload-bar 15s linear", html) # the bar's length is the refresh interval
23 self.assertIn("counter-reset: reload-left var(--reload-left)", html) # the text countdown is CSS-only
24 self.assertIn("steps(15)", html) # one step a second
25 self.assertNotIn("<script", html) # the site's CSP is script-src 'none'
26 self.assertIn("prefers-reduced-motion", html)
27
28 def test_page_without_refresh_has_none_of_it(self):
29 html = gen.page("T", "<p>x</p>")
30 for marker in ("http-equiv", "reload-bar", "reload-in"):
31 self.assertNotIn(marker, html)
32
33 def test_reload_timer_empty_without_interval(self):
34 self.assertEqual(gen.reload_timer(None), "")
35 self.assertEqual(gen.reload_timer(0), "")
36
37
ced0eafa 2026-11-04 Pavel Safronov38PRICES = {"as_of": "2026-10-22", "models": {
39 "m-big": {"input": 10, "output": 50, "cache_write_5m": 12, "cache_write_1h": 20, "cache_read": 1},
40 "m-small": {"input": 1, "output": 5, "cache_write_5m": 1, "cache_write_1h": 2, "cache_read": 0.1,
41 "above_100k": {"input": 2, "output": 10, "cache_write_5m": 2, "cache_write_1h": 4, "cache_read": 0.2}}}}
42
43
44def commit(sha, date, message, author="a"):
45 return ([sha, sha[:3], author, date, "subject"], message)
46
47
48def trailers(i, o, c, models=None, method=None):
49 t = f"x\n\nFW-Tokens-In: {i}\nFW-Tokens-Out: {o}\nFW-Tokens-Cached: {c}\n"
50 if method:
51 t += f"FW-Tokens-Method: {method}\n"
52 for m, v in (models or {}).items():
53 t += f"FW-Model-Tokens: {m} in={v[0]} out={v[1]} read={v[2]} write5m={v[3]} write1h={v[4]}\n"
54 return t
55
56
57class UsageCosts(unittest.TestCase):
58 def test_parse_model_usage(self):
59 msg = trailers(1, 2, 3, {"m-big": [1, 2, 3, 4, 5], "m-small": [0, 0, 0, 0, 0]}, 2)
60 self.assertEqual(gen.parse_model_usage(msg), {"m-big": [1, 2, 3, 4, 5], "m-small": [0, 0, 0, 0, 0]})
61 old = "x\n\nFW-Tokens-In: 1\nFW-Tokens-Out: 2\nFW-Tokens-Cached: 3\n"
62 self.assertIsNone(gen.parse_model_usage(old))
63 self.assertIsNone(gen.parse_model_usage("Usage: 1.0K in / 2.0K out / 3.0K cached (session tally)"))
64 self.assertEqual(gen.parse_usage(old), ([1.0, 2.0, 3.0], [0, 0, 0])) # unchanged
65 self.assertEqual(gen.usage_method(old), 1)
66 self.assertEqual(gen.usage_method(msg), 2)
67
68 def test_commit_cost_arithmetic(self):
69 cost, unknown = gen.commit_cost({"m-big": [1_000_000, 100_000, 2_000_000, 10_000, 5_000]}, PRICES)
70 self.assertAlmostEqual(cost, 10 + 5 + 2 + 0.12 + 0.1)
71 self.assertEqual(unknown, [])
72 cost, _ = gen.commit_cost({"m-small@over100k": [1_000_000, 0, 0, 0, 0], "m-small": [0, 0, 1_000_000, 0, 0]}, PRICES)
73 self.assertAlmostEqual(cost, 2 + 0.1)
74
75 def test_unknown_models_and_no_data(self):
76 self.assertEqual(gen.commit_cost(None, PRICES), (None, []))
77 self.assertEqual(gen.commit_cost({"m-big": [1, 1, 1, 1, 1], "m-new": [1, 0, 0, 0, 0]}, PRICES), (None, ["m-new"]))
78 self.assertEqual(gen.commit_cost({"m-new": [0, 0, 0, 0, 0]}, PRICES), (0.0, [])) # zero tokens need no price
79
80 def test_compute_costs_per_model_deltas(self):
81 costs, total, allc = gen.compute_costs([
82 commit("aaa", "2026-10-26", trailers(10, 100, 1000, {"m-big": [10, 100, 1000, 0, 0]}, 2)),
83 commit("bbb", "2026-10-27", "plain"),
84 commit("ccc", "2026-10-28", trailers(30, 300, 3000, {"m-big": [20, 250, 2500, 0, 0], "m-small": [10, 50, 500, 0, 0]}, 2)),
85 ], PRICES)
86 self.assertEqual(total, 3)
87 self.assertTrue(costs["aaa"]["new"] and not costs["ccc"]["new"])
88 self.assertEqual(costs["ccc"]["models"], {"m-big": [10, 150, 1500, 0, 0], "m-small": [10, 50, 500, 0, 0]})
89 self.assertEqual(costs["ccc"]["delta"], [20, 200, 2000])
90 self.assertAlmostEqual(costs["ccc"]["cost"], (100 + 7500 + 1500 + 10 + 250 + 50) / 1e6)
91 self.assertEqual([c["has_tokens"] for c in allc], [True, False, True])
92
93 def test_method_change_and_fallen_counter_start_new_session(self):
94 m = {"m-big": [10, 100, 1000, 0, 0]}
95 costs, _, _ = gen.compute_costs([
96 commit("aaa", "2026-10-26", trailers(10, 100, 1000)), # method 1
97 commit("bbb", "2026-10-27", trailers(20, 200, 2000, m, 2)), # method 2: new session
98 commit("ccc", "2026-10-28", trailers(25, 300, 3000, {"m-big": [10, 90, 1200, 0, 0]}, 2)), # model out fell
99 ], PRICES)
100 self.assertEqual([costs[k]["new"] for k in ("aaa", "bbb", "ccc")], [True, True, True])
101 self.assertEqual([costs[k]["session"] for k in ("aaa", "bbb", "ccc")], [1, 2, 3])
102 self.assertEqual(costs["bbb"]["delta"], [20, 200, 2000])
103 self.assertIsNone(costs["aaa"]["cost"]) # old commit: tokens shown, cost unknown
104
105 def test_unknown_model_gives_unknown_cost(self):
106 costs, _, _ = gen.compute_costs(
107 [commit("aaa", "2026-10-26", trailers(1, 1, 1, {"m-new": [1, 1, 1, 0, 0]}, 2))], PRICES)
108 self.assertIsNone(costs["aaa"]["cost"])
109 self.assertEqual(costs["aaa"]["unknown"], ["m-new"])
110 self.assertIn("m-new", gen.cost_text(costs["aaa"]))
111
112 def test_ai_marker(self):
113 for line in ("Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>", "co-authored-by: X <x@openai.com>",
114 "Co-authored-by: Copilot <c@github.com>"):
115 self.assertTrue(gen.AI_COAUTHOR_RE.search("msg\n\n" + line), line)
116 self.assertFalse(gen.AI_COAUTHOR_RE.search("Co-Authored-By: Bob <bob@example.com>"))
117 self.assertFalse(gen.AI_COAUTHOR_RE.search("Claude helped with this"))
118 _, _, allc = gen.compute_costs([commit("aaa", "2026-10-26", "x\n\nCo-Authored-By: Claude <noreply@anthropic.com>"),
119 commit("bbb", "2026-10-26", "x")], PRICES)
120 self.assertEqual([c["ai"] for c in allc], [True, False])
121
122
123class Charts(unittest.TestCase):
124 def buckets(self):
125 costs, _, allc = gen.compute_costs([
126 commit("aaa", "2026-10-26", trailers(10, 100, 1000, {"m-big": [10, 100, 1000, 0, 0]}, 2)), # Monday
127 commit("bbb", "2026-10-28", "plain"),
128 commit("ccc", "2026-11-12", trailers(20, 300, 2000, {"m-big": [20, 300, 2000, 0, 0]}, 2)),
129 ], PRICES)
130 return gen.week_buckets(allc, costs)
131
132 def test_week_buckets_keep_empty_weeks_and_flag_unknown(self):
133 b = self.buckets()
134 self.assertEqual([x["label"] for x in b], ["week of 2026-10-26", "week of 2026-11-02", "week of 2026-11-09"])
135 self.assertEqual([x["commits"] for x in b], [2, 0, 1])
136 self.assertEqual([x["nodata"] for x in b], [1, 0, 0])
137 self.assertEqual([x["with_tokens"] for x in b], [1, 0, 1])
138 self.assertEqual(b[0]["out"], 100)
139 self.assertEqual(b[0]["with_cost"], 1)
140
141 def test_long_history_buckets_by_month(self):
142 _, _, allc = gen.compute_costs([commit("aaa", "2020-01-15", "x"), commit("bbb", "2026-10-26", "x")], PRICES)
143 b = gen.week_buckets(allc, {})
144 self.assertEqual(b[0]["label"], "month of 2020-01")
145 self.assertEqual(len(b), 82)
146
799905ae 2026-11-04 Pavel Safronov147 def test_line_chart_svg(self):
ced0eafa 2026-11-04 Pavel Safronov148 b = self.buckets()
799905ae 2026-11-04 Pavel Safronov149 svg = gen.line_chart_svg([x["label"] for x in b], [x["out"] if x["with_tokens"] else None for x in b],
150 fmt=lambda v: f"{gen.fmt_tokens(v)} output tokens",
151 unknown=[(x["nodata"], x["commits"]) for x in b], aria="Output per week")
ced0eafa 2026-11-04 Pavel Safronov152 self.assertNotIn("<script", svg)
153 self.assertIn('viewBox="0 0 720 200"', svg)
154 self.assertIn("<title>week of 2026-10-26: 100 output tokens, 1 of 2 commits with data</title>", svg)
799905ae 2026-11-04 Pavel Safronov155 self.assertIn("<title>week of 2026-11-02: no data, no commits</title>", svg) # empty weeks keep a hover target
ced0eafa 2026-11-04 Pavel Safronov156 self.assertEqual(svg.count('class="unk"'), 1) # only the first week has commits without data
799905ae 2026-11-04 Pavel Safronov157 self.assertEqual(svg.count('class="hit"'), 3)
158 self.assertEqual(svg.count("<circle"), 2) # two known points; the gap week has none
ced0eafa 2026-11-04 Pavel Safronov159 self.assertIn("<pattern", svg)
160 self.assertIn("hatched = commits without usage data (unknown, not zero)", svg)
161 self.assertIn(">2026-10-26<", svg)
162 self.assertIn(">2026-11-09<", svg)
163
799905ae 2026-11-04 Pavel Safronov164 def chart(self, values, **kw):
165 labels = [f"week of 2026-10-{26 + 7 * i:02d}" if i < 1 else f"week of 2026-11-{(i - 1) * 7 + 2:02d}" for i in range(len(values))]
166 return gen.line_chart_svg(labels, values, fmt=str, unknown=[(0, 1)] * len(values), aria="a", **kw)
167
168 def test_gap_splits_the_line_into_segments(self):
169 svg = self.chart([1, 2, None, 3, 4, None, 5])
170 self.assertEqual(svg.count('<path class="line"'), 2) # [1,2] and [3,4]; the lone 5 is only a dot
171 self.assertEqual(svg.count('<path class="area"'), 2)
172 self.assertEqual(svg.count("<circle"), 5)
173 self.assertNotIn("NaN", svg)
174
175 def test_all_unknown_has_no_path_or_dot(self):
176 svg = self.chart([None, None])
177 self.assertNotIn('<path class="line"', svg)
178 self.assertNotIn("<circle", svg)
179 self.assertEqual(svg.count('class="hit"'), 2)
180
181 def test_single_point_is_a_dot(self):
182 svg = self.chart([7])
183 self.assertEqual(svg.count("<circle"), 1)
184 self.assertNotIn('<path class="line"', svg)
185
186 def test_known_zero_is_drawn_on_the_axis(self):
187 svg = self.chart([0, 0])
188 self.assertEqual(svg.count('<path class="line"'), 1)
189 self.assertEqual(svg.count("<circle"), 2)
190
191 def test_two_series_and_legend(self):
192 svg = self.chart([3, 4], second=[1, 2], legend=[("All commits", "muted"), ("AI-marked commits", "")], hatch_note="")
193 self.assertIn('<path class="line muted"', svg)
194 self.assertIn('<path class="line"', svg)
195 self.assertIn(">All commits<", svg)
196 self.assertIn(">AI-marked commits<", svg)
197
198 def test_titles_are_escaped(self):
199 svg = gen.line_chart_svg(["week of 2026-10-26"], [1], fmt=str, unknown=[(0, 1)], aria="<i>", tips=["<b>x</b>"])
200 self.assertNotIn("<b>", svg)
201 self.assertNotIn("<i>", svg)
202 self.assertIn("&lt;b&gt;x&lt;/b&gt;", svg)
203
ced0eafa 2026-11-04 Pavel Safronov204
205class SessionBreaks(unittest.TestCase):
206 def test_vanished_model_starts_a_new_session(self):
207 costs, _, _ = gen.compute_costs([
208 commit("aaa", "2026-10-26", trailers(10, 100, 1000, {"m-big": [10, 100, 1000, 0, 0]}, 2)),
209 commit("bbb", "2026-10-27", trailers(20, 250, 2000, {"m-small": [20, 150, 2000, 0, 0]}, 2)),
210 ], PRICES)
211 self.assertTrue(costs["bbb"]["new"])
212 self.assertEqual(costs["bbb"]["models"], {"m-small": [20, 150, 2000, 0, 0]})
213 self.assertEqual(costs["bbb"]["delta"], [20, 250, 2000])
214
215 def test_model_data_after_commits_without_it_starts_a_new_session(self):
216 costs, _, _ = gen.compute_costs([
217 commit("aaa", "2026-10-26", trailers(10, 100, 1000, None, 2)),
218 commit("bbb", "2026-10-27", trailers(20, 250, 2000, {"m-big": [20, 250, 2000, 0, 0]}, 2)),
219 ], PRICES)
220 self.assertTrue(costs["bbb"]["new"])
221 self.assertEqual(costs["bbb"]["delta"], [20, 250, 2000])
222 self.assertEqual(costs["bbb"]["models"], {"m-big": [20, 250, 2000, 0, 0]})
223
224
799905ae 2026-11-04 Pavel Safronov225class UsageFixtures:
226 """setUp and helpers for tests that render usage pages from fake commits (no test_ methods of its own)."""
ced0eafa 2026-11-04 Pavel Safronov227 def setUp(self):
228 self._cache = dict(gen._usage_cache)
229 self.addCleanup(lambda: (gen._usage_cache.clear(), gen._usage_cache.update(self._cache)))
799905ae 2026-11-04 Pavel Safronov230 self.state = tempfile.mkdtemp()
231 self.addCleanup(shutil.rmtree, self.state, True)
232 patcher = mock.patch.object(gen, "STATE", self.state)
233 patcher.start()
234 self.addCleanup(patcher.stop)
ced0eafa 2026-11-04 Pavel Safronov235
236 def usage(self, commits, name="r"):
237 costs, total, allc = gen.compute_costs(commits, PRICES)
238 known = [c["cost"] for c in costs.values() if c["cost"] is not None]
239 gen._usage_cache[name] = {"costs": costs, "commits": total, "all": allc, "median": 1, "max": 1000,
240 "dollars": sum(known), "priced": len(known)}
241 return gen._usage_cache[name]
242
243 def page(self, commits):
244 self.usage(commits)
245 written = {}
246 view = gen.View("r", "main", True)
247 with mock.patch.object(gen, "write", lambda path, content: written.update({path: content})), \
248 mock.patch.object(gen, "load_prices", return_value=PRICES), \
249 mock.patch.object(gen, "ci_pipelines", return_value={}):
250 gen.build_usage_page(view, None)
251 return next(iter(written.values()))
252
799905ae 2026-11-04 Pavel Safronov253
254
255class UsageRendering(UsageFixtures, unittest.TestCase):
ced0eafa 2026-11-04 Pavel Safronov256 def test_cell_and_panel_label_cost_and_never_show_zero_for_unknown(self):
257 u = self.usage([commit("a" * 40, "2026-10-26", trailers(1, 1000, 5, {"m-big": [1, 1000, 5, 0, 0]}, 2)),
258 commit("b" * 40, "2026-10-27", "x\n\nFW-Tokens-In: 2\nFW-Tokens-Out: 2000\nFW-Tokens-Cached: 9\n")])
259 with mock.patch.object(gen, "load_prices", return_value=PRICES):
260 cell = gen.usage_cell(u, "a" * 40)
261 self.assertIn("est. $0.05", cell)
262 self.assertIn("not what was paid", cell)
263 self.assertIn("2026-10-22", cell)
264 panel = gen.usage_panel("r", "a" * 40)
265 self.assertIn("not what was paid", panel)
266 old_cell, old_panel = gen.usage_cell(u, "b" * 40), gen.usage_panel("r", "b" * 40)
267 for html_ in (old_cell, old_panel):
268 self.assertIn("cost unknown", html_)
269 self.assertNotIn("$0", html_)
270 self.assertNotIn("est. $", html_)
271
272 def test_usage_page_labels_and_hostile_model_name(self):
273 html_ = self.page([commit("a" * 40, "2026-10-26", trailers(1, 1000, 5, {"m-big": [1, 1000, 5, 0, 0]}, 2)),
274 commit("b" * 40, "2026-10-27", trailers(2, 2000, 9, {"<b>x</b>": [1, 1, 1, 0, 0]}, 2))])
275 self.assertIn("not what was paid", html_)
276 self.assertIn("Output tokens per week", html_)
277 self.assertNotIn("<b>x</b>", html_)
278 self.assertIn("&lt;b&gt;x&lt;/b&gt;", html_) # in the tooltip of the unpriced commit
279 self.assertIn("session starts", html_)
280 self.assertNotIn("<script", html_)
281
282 def test_usage_page_old_commits_only_has_unknown_cost(self):
283 html_ = self.page([commit("a" * 40, "2026-10-26", "x\n\nFW-Tokens-In: 2\nFW-Tokens-Out: 2000\nFW-Tokens-Cached: 9\n")])
284 self.assertIn("cost unknown", html_)
285 self.assertNotIn("est. $", html_)
286
287 def test_usage_page_monthly_heading_for_long_history(self):
288 html_ = self.page([commit("a" * 40, "2020-01-06", trailers(1, 1000, 5, {"m-big": [1, 1000, 5, 0, 0]}, 2)),
289 commit("b" * 40, "2026-10-27", "plain")])
290 self.assertIn("Output tokens per month", html_)
291 self.assertNotIn("per week", html_)
292
293 def test_week_104_vs_105_boundary(self):
294 def labels(last):
295 _, _, allc = gen.compute_costs([commit("a" * 40, "2024-01-01", "x"), commit("b" * 40, last, "x")], PRICES)
296 return gen.week_buckets(allc, {})
297 b104 = labels("2025-12-22") # Monday 103 weeks later: 104 weeks
298 b105 = labels("2025-12-29") # 105 weeks
299 self.assertEqual(len(b104), 104)
300 self.assertTrue(b104[0]["label"].startswith("week of"))
301 self.assertTrue(b105[0]["label"].startswith("month of"))
302 self.assertEqual(gen.bucket_unit(b104), "week")
303 self.assertEqual(gen.bucket_unit(b105), "month")
304
305 def test_chart_hover_target_and_unique_pattern(self):
799905ae 2026-11-04 Pavel Safronov306 svg = gen.line_chart_svg(["week of 2026-10-26", "week of 2026-11-02"], [None, 5], fmt=str, unknown=[(1, 1), (0, 1)],
307 aria="a", pattern_id="p2")
ced0eafa 2026-11-04 Pavel Safronov308 self.assertIn('id="p2"', svg)
309 self.assertIn("url(#p2)", svg)
310 self.assertEqual(svg.count('class="hit"'), 2)
799905ae 2026-11-04 Pavel Safronov311 self.assertEqual(svg.count("<circle"), 1)
312
313class UsageChartsAndSiteRollup(UsageFixtures, unittest.TestCase):
314 BIG = {"m-big": [1, 1000, 5, 0, 0]}
315 OLD = "x\n\nFW-Tokens-In: 2\nFW-Tokens-Out: 4000\nFW-Tokens-Cached: 9\n" # tokens, no per-model lines: cost unknown
316
317 def captured(self, fn, *args):
318 written, real = {}, gen.write
319 def fake(path, content):
320 (real if path.startswith(self.state) else lambda p, c: written.update({p: c}))(path, content)
321 with mock.patch.object(gen, "write", fake), \
322 mock.patch.object(gen, "load_prices", return_value=PRICES), \
323 mock.patch.object(gen, "ci_pipelines", return_value={}):
324 fn(*args)
325 return written
326
327 def check_page(self, html_, charts=5):
328 ids = re.findall(r'<pattern id="([^"]+)"', html_)
329 self.assertEqual(len(ids), len(set(ids))) # unique pattern ids
330 self.assertEqual(html_.count('<svg class="chart"'), charts)
331 self.assertNotIn("<script", html_)
332 self.assertIsNone(re.search(r"\$0(\.00?)?(?![.\d])", html_)) # unknown is never drawn as zero dollars
333 if "$" in html_:
334 self.assertIn("not what was paid", html_)
335
336 def repo(self, name, commits):
337 self.usage(commits, name)
338
339 def site(self, names, today=None):
340 with mock.patch.object(gen, "published_public_repos", return_value=[(n, "") for n in names]):
341 written = self.captured(gen.build_site_usage)
342 return next(iter(written.values()))
343
344 def test_repo_page_has_all_charts_and_sessions_cost(self):
345 html_ = self.page([commit("a" * 40, "2026-10-26", trailers(1, 1000, 5, self.BIG, 2) + "Co-Authored-By: Claude <noreply@anthropic.com>\n"),
346 commit("b" * 40, "2026-10-27", "plain")])
347 for h in ("Output tokens per week", "Estimated cost per week", "Cached reads per week",
348 "Cost per commit, average per week", "AI-marked commits per week"):
349 self.assertIn(f"<h2>{h}</h2>", html_)
350 self.check_page(html_)
351 self.assertIn("n=1 commit with a known cost", html_)
352 self.assertIn("1 AI-marked, 1 with no AI marker", html_)
353 self.assertIn('<circle class="dot muted"', html_) # one week: dots only; all commits (grey) ...
354 self.assertIn('<circle class="dot"', html_) # ... and the AI-marked series
355 self.assertIn(">AI-marked commits<", html_) # the two-line AI chart's legend
356 self.assertIn("<h2>AI-marked commits per week</h2>", html_)
357 self.assertIn("no AI marker", html_)
358 self.assertIn("Est. cost (not what was paid)", html_)
359 self.assertIn(">est. $0.05</td>", html_)
360 self.assertIn('<a href="/usage/">Usage across all repositories</a>', html_)
361 self.assertIn('<dl class="stats">', html_)
362 self.assertIn("<dt>Commits with usage data</dt><dd>1 of 2</dd>", html_)
363 self.assertIn("est. $0.05 over the 1 commit with a known cost", html_) # singular
364 self.assertIn("estimated API list-price equivalent at prices of 2026-10-22, not what was paid", html_)
365
366 def test_session_cost_known_only_if_every_commit_has_one(self):
367 costs, _, _ = gen.compute_costs([
368 commit("a" * 40, "2026-10-26", trailers(1, 1000, 5, self.BIG, 2)),
369 commit("b" * 40, "2026-10-27", trailers(2, 2000, 9, {"m-new": [1, 1, 1, 0, 0]}, 2))], PRICES)
370 both = [costs["a" * 40], costs["b" * 40]]
371 self.assertIsNone(gen.session_cost(both))
372 self.assertAlmostEqual(gen.session_cost(both[:1]), 0.050015)
373 with mock.patch.object(gen, "load_prices", return_value=PRICES):
374 self.assertIn(">unknown</td>", gen.session_cost_cell(both))
375 self.assertNotIn("$", gen.session_cost_cell(both))
376 self.assertIn("not what was paid", gen.session_cost_cell(both[:1]))
377
378 def test_page_without_known_costs_says_unknown_not_zero(self):
379 html_ = self.page([commit("a" * 40, "2026-10-26", self.OLD)])
380 self.assertIn("No commit has a known cost yet", html_)
381 self.assertNotIn("$", html_) # no dollar figure at all, in particular no $0
382 self.assertNotIn("<script", html_)
383 self.check_page(html_, charts=3) # tokens, cached reads, AI-marked; the two cost charts say so
384 self.assertIn('<td class="num" title="at least one commit in this session has no known cost">unknown</td>', html_)
385
386 def test_known_zero_is_a_value_unknown_is_not(self):
387 args = dict(pattern_id="z", empty="nothing", fmt=str, unknown=[(0, 1)], aria="a")
388 self.assertIn("<svg", gen.usage_chart("T", "c", ["week of 2026-10-26"], [0], **args))
389 self.assertNotIn("<svg", gen.usage_chart("T", "c", ["week of 2026-10-26"], [None], **args))
390 self.assertIn("nothing", gen.usage_chart("T", "c", ["week of 2026-10-26"], [None], **args))
391
392 def test_empty_buckets_give_messages_not_axes(self):
393 out = gen.usage_charts([], "e")
394 self.assertNotIn("<svg", out)
395 self.assertIn("No commits yet.", out)
396 self.assertEqual(gen.line_chart_svg([], [], fmt=str, unknown=[], aria="a"), "")
397
398 def test_site_rollup_aggregates_two_repos(self):
399 self.repo("one", [commit("a" * 40, "2026-10-26", trailers(1, 1000, 5, self.BIG, 2)),
400 commit("b" * 40, "2026-10-27", "plain")])
401 self.repo("two", [commit("a" * 40, "2026-10-28", self.OLD),
402 commit("c" * 40, "2026-11-03", "x\n\nCo-Authored-By: Claude <noreply@anthropic.com>")])
403 self.repo("none", [commit("d" * 40, "2026-10-29", "plain")])
404 buckets, rows, start = gen.site_usage_totals(["one", "two", "none"], today=datetime.date(2026, 11, 4))
405 self.assertEqual(start, "2026-10-26")
406 self.assertEqual([r["name"] for r in rows], ["two", "one", "none"]) # by output tokens
407 self.assertEqual(buckets[0]["commits"], 4) # all four commits of the first week; "a"*40 exists in both repos
408 self.assertEqual(buckets[0]["out"], 5000)
409 self.assertEqual(buckets[0]["with_cost"], 1)
410 self.assertEqual(buckets[1]["ai"], 1)
411 html_ = self.site(["one", "two", "none"])
412 self.check_page(html_)
413 self.assertIn("Starts at the first AI-marked commit, 2026-10-26.", html_)
414 cost = 'title="estimated API list-price equivalent, not what was paid"'
415 self.assertIn('<tr><td><a href="/two/usage/">two</a></td><td class="num">2</td><td class="num">1</td>'
416 '<td class="num">100%</td><td class="num">4.0K</td>'
417 '<td class="num" title="no commit in this repo has a known cost">unknown</td></tr>', html_)
418 self.assertIn('<tr><td><a href="/one/usage/">one</a></td><td class="num">2</td><td class="num">1</td>'
419 f'<td class="num">50%</td><td class="num">1.0K</td><td class="num" {cost}>est. $0.05</td></tr>', html_)
420 self.assertIn('<tr><td>none</td><td class="num">1</td><td class="num">0</td><td class="num">0%</td>'
421 '<td class="num">unknown</td><td class="num" title="no commit in this repo has a known cost">unknown</td></tr>', html_)
422
423 def test_site_cost_cell_shows_coverage_when_partly_known(self):
424 self.repo("p", [commit("a" * 40, "2026-10-26", trailers(1, 1000, 5, self.BIG, 2)),
425 commit("b" * 40, "2026-10-27", trailers(2, 2000, 9, {"m-new": [1, 1, 1, 0, 0]}, 2))])
426 self.assertIn("est. $0.05 (1 of 2 commits)</td>", self.site(["p"]))
427
428 def test_site_range_starts_at_first_ai_commit_and_ignores_bogus_dates(self):
429 self.repo("a", [commit("a" * 40, "1970-01-01", "x\n\nCo-Authored-By: Claude <noreply@anthropic.com>"),
430 commit("b" * 40, "2031-01-01", "x\n\nCo-Authored-By: Claude <noreply@anthropic.com>"),
431 commit("c" * 40, "2020-01-06", "plain"),
432 commit("d" * 40, "2026-10-26", "x\n\nCo-Authored-By: Claude <noreply@anthropic.com>")])
433 buckets, rows, start = gen.site_usage_totals(["a"], today=datetime.date(2026, 11, 4))
434 self.assertEqual(start, "2026-10-26")
435 self.assertEqual(len(buckets), 1)
436 self.assertEqual(rows[0]["commits"], 4) # the table still counts every commit
437 self.repo("b", [commit("a" * 40, "1970-01-01", "x\n\nCo-Authored-By: Claude <noreply@anthropic.com>"),
438 commit("c" * 40, "2026-10-26", "plain")])
439 buckets, _, start = gen.site_usage_totals(["b"], today=datetime.date(2026, 11, 4))
440 self.assertEqual((buckets, start), ([], None))
441 self.assertNotIn("Starts at", self.site(["b"]))
442
443 def test_site_page_buckets_by_month_for_long_histories(self):
444 self.repo("old", [commit("a" * 40, "2020-01-06", "x\n\nCo-Authored-By: Claude <noreply@anthropic.com>")])
445 self.repo("new", [commit("a" * 40, "2026-10-26", trailers(1, 1000, 5, self.BIG, 2))])
446 html_ = self.site(["old", "new"])
447 self.assertIn("<h2>Output tokens per month</h2>", html_)
448 self.assertNotIn("per week", html_)
449
450 def test_site_page_escapes_repo_names(self):
451 self.repo("<b>x</b>", [commit("a" * 40, "2026-10-26", trailers(1, 1000, 5, self.BIG, 2))])
452 html_ = self.site(["<b>x</b>"])
453 self.assertNotIn("<b>x</b>", html_)
454 self.assertIn("&lt;b&gt;x&lt;/b&gt;", html_)
455
456 def test_stats_plural_and_unknown(self):
457 self.assertEqual(gen.plural(1, "commit"), "1 commit")
458 self.assertEqual(gen.plural(2, "commit"), "2 commits")
459 self.assertIn("2 commits with a known cost", gen.cost_stat(1.5, 2, "d")[1])
460 label, text = gen.cost_stat(0.0, 0, "d")
461 self.assertIn("unknown, not zero", text)
462 self.assertNotIn("$", text)
463
464 def test_site_page_header_and_nav(self):
465 self.repo("one", [commit("a" * 40, "2026-10-26", trailers(1, 1000, 5, self.BIG, 2))])
466 html_ = self.site(["one"])
467 self.assertIn("<dt>Repositories</dt><dd>1</dd>", html_)
468 self.assertIn('<a href="/usage/" aria-current="page">Usage</a>', html_)
469 self.assertNotIn('<a href="/" aria-current="page"', html_)
470 self.assertIn(["Usage", "https://code.fuzzyworld.net/usage/"], gen.site_nav())
471
472 def test_site_rollup_with_no_repos(self):
473 html_ = self.site([])
474 self.assertIn("No public repository is published yet.", html_)
475 self.assertNotIn("<svg", html_)
476 self.assertNotIn("<script", html_)
477
478 def test_summary_cache_hit_head_change_missing_and_corrupt(self):
479 self.repo("c", [commit("a" * 40, "2026-10-26", trailers(1, 1000, 5, self.BIG, 2))])
480 path = os.path.join(self.state, "c.usage.json")
481 head = ["h1"]
482 with mock.patch.object(gen, "git_text", lambda *a, **k: head[0]), \
483 mock.patch.object(gen, "repo_usage", wraps=gen.repo_usage) as ru:
484 first = gen.usage_summary("c")
485 self.assertEqual((ru.call_count, os.path.exists(path)), (1, True))
486 self.assertEqual(first["rows"][0][:3], ["2026-10-26", 1, 1000])
487 gen.usage_summary("c")
488 self.assertEqual(ru.call_count, 1) # hit: no git work
489 head[0] = "h2"
490 gen.usage_summary("c")
491 self.assertEqual(ru.call_count, 2) # HEAD changed: recomputed
492 os.remove(path)
493 gen.usage_summary("c")
494 self.assertEqual(ru.call_count, 3) # missing file
495 with open(path, "w") as f:
496 f.write("{not json")
497 self.assertEqual(gen.usage_summary("c")["rows"], first["rows"])
498 self.assertEqual(ru.call_count, 4) # corrupt file
499 with open(path, "w") as f:
500 json.dump({"head": "h2", "key": gen.gen_key(), "rows": [[1]]}, f)
501 gen.usage_summary("c")
502 self.assertEqual(ru.call_count, 5) # malformed rows
503 gen.usage_summary("c")
504 self.assertEqual(ru.call_count, 5) # and it was rewritten properly
505
506 def test_published_public_repos(self):
507 root, site = tempfile.mkdtemp(), tempfile.mkdtemp()
508 self.addCleanup(shutil.rmtree, root, True)
509 self.addCleanup(shutil.rmtree, site, True)
510 for name, vis, published in [("ok", "public", True), ("priv", "private", True), ("unpub", "public", False),
511 ("cap", "Public", True), ("spaced", "public ", True), ("usage", "public", True),
512 ("ci", "public", True)]:
513 subprocess.run(["git", "init", "-q", "--bare", os.path.join(root, name + ".git")], check=True)
514 subprocess.run(["git", "-C", os.path.join(root, name + ".git"), "config", "fuzzyworld.visibility", vis], check=True)
515 if published:
516 os.makedirs(os.path.join(site, name))
517 open(os.path.join(site, name, "index.html"), "w").close()
518 with mock.patch.object(gen, "GIT_ROOT", root), mock.patch.object(gen, "SITE", site):
519 self.assertEqual([n for n, _ in gen.published_public_repos()], ["ok", "spaced"])
520 # "public " (trailing space) is still public: repo_config strips whitespace, as before this change.
521 # "Public" (case) is private, as are unpublished repos, stale SITE pages of private repos, and reserved names.
522
523 def test_index_links_to_site_usage(self):
524 written = {}
525 with mock.patch.object(gen, "write", lambda p, c: written.update({p: c})), \
526 mock.patch.object(gen, "build_ci_dashboard"), mock.patch.object(gen, "published_public_repos", return_value=[]), \
527 mock.patch.object(gen, "build_site_usage") as bsu:
528 gen.build_index()
529 self.assertIn('<a href="/usage/">', next(iter(written.values())))
530 bsu.assert_called_once()
ced0eafa 2026-11-04 Pavel Safronov531
392edf54 2026-11-04 Pavel Safronov532 def _repo_committed_at(self, root, name, date):
533 """A public bare repo under root whose only commit is dated `date` (ISO)."""
534 work = tempfile.mkdtemp()
535 self.addCleanup(shutil.rmtree, work, True)
536 env = {**os.environ, "GIT_AUTHOR_DATE": date, "GIT_COMMITTER_DATE": date, "GIT_AUTHOR_NAME": "t",
537 "GIT_AUTHOR_EMAIL": "t@x", "GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@x"}
538 subprocess.run(["git", "init", "-q", work], check=True)
539 subprocess.run(["git", "-C", work, "commit", "-q", "--allow-empty", "-m", "c"], check=True, env=env)
540 bare = os.path.join(root, name + ".git")
541 subprocess.run(["git", "clone", "-q", "--bare", work, bare], check=True)
542 subprocess.run(["git", "-C", bare, "config", "fuzzyworld.visibility", "public"], check=True)
543 return bare
544
545 def test_indexes_are_tables_ordered_by_newest_commit_not_deploy(self):
546 root, apps = tempfile.mkdtemp(), tempfile.mkdtemp()
547 self.addCleanup(shutil.rmtree, root, True)
548 self.addCleanup(shutil.rmtree, apps, True)
549 old = self._repo_committed_at(root, "old", "2026-01-01T12:00:00Z")
550 new = self._repo_committed_at(root, "new", "2026-06-01T12:00:00Z")
551 # "old" was deployed most recently, but its code is older: it must still be listed second.
552 for app, deployed in [("old", "2026-11-03T00:00:00Z"), ("new", "2026-02-01T00:00:00Z")]:
553 os.makedirs(os.path.join(apps, app))
554 with open(os.path.join(apps, app, ".fuzzyworld.json"), "w") as f:
555 json.dump({"title": app, "repo": app, "deployed": deployed}, f)
556 written = {}
557 with mock.patch.object(gen, "write", lambda p, c: written.update({p: c})), mock.patch.object(gen, "GIT_ROOT", root), \
558 mock.patch.object(gen, "APPS", apps), mock.patch.object(gen, "build_ci_dashboard"), \
559 mock.patch.object(gen, "build_site_usage"), \
560 mock.patch.object(gen, "published_public_repos", return_value=[("old", old), ("new", new)]):
561 gen.build_index()
562 gen.build_apps_index()
563 for html_ in written.values():
564 self.assertIn("<table", html_)
565 self.assertNotIn('<ul class="repos"', html_)
566 self.assertLess(html_.index('>new</a>'), html_.index('>old</a>'))
567 self.assertIn("2026-06-01", written[os.path.join(apps, "index.html")])
568 self.assertIn("2026-11-03", written[os.path.join(apps, "index.html")]) # deploy date shown, not used for order
569 self.assertNotIn('class="date"', "".join(written.values())) # .log td.date is hidden on phones; headers would misalign
570
571 def test_apps_index_leaves_last_commit_blank_when_repo_missing_and_tolerates_null_fields(self):
572 root, apps = tempfile.mkdtemp(), tempfile.mkdtemp()
573 self.addCleanup(shutil.rmtree, root, True)
574 self.addCleanup(shutil.rmtree, apps, True)
575 os.makedirs(os.path.join(apps, "gone"))
576 with open(os.path.join(apps, "gone", ".fuzzyworld.json"), "w") as f:
577 json.dump({"title": "gone", "repo": "gone", "deployed": "2026-11-03T00:00:00Z", "description": None}, f)
578 written = {}
579 with mock.patch.object(gen, "write", lambda p, c: written.update({p: c})), mock.patch.object(gen, "GIT_ROOT", root), \
580 mock.patch.object(gen, "APPS", apps):
581 gen.build_apps_index()
582 row = re.search(r"<tr><td class=\"shot\">.*?</tr>", written[os.path.join(apps, "index.html")]).group(0)
583 cells = re.findall(r'<td class="num">(.*?)</td>', row)
584 self.assertEqual(cells, ["", "2026-11-03"]) # last commit blank, deployed shown
585
ced0eafa 2026-11-04 Pavel Safronov586
b447a9f2 2026-11-03 Pavel Safronov587if __name__ == "__main__":
588 unittest.main()