fuzzyworld-tools / files / tests / test_gen.py
test_gen.py
b447a9f2 2026-11-03 Pavel Safronov | 1 | """Tests for the page furniture in server/gen.py. Run: python3 -m unittest discover tests""" |
799905ae 2026-11-04 Pavel Safronov | 2 | import datetime |
| 3 | import json | |
b447a9f2 2026-11-03 Pavel Safronov | 4 | import os |
799905ae 2026-11-04 Pavel Safronov | 5 | import re |
| 6 | import shutil | |
| 7 | import subprocess | |
b447a9f2 2026-11-03 Pavel Safronov | 8 | import sys |
799905ae 2026-11-04 Pavel Safronov | 9 | import tempfile |
b447a9f2 2026-11-03 Pavel Safronov | 10 | import unittest |
ced0eafa 2026-11-04 Pavel Safronov | 11 | from unittest import mock |
b447a9f2 2026-11-03 Pavel Safronov | 12 | |
| 13 | sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "server")) | |
| 14 | import gen # noqa: E402 | |
| 15 | ||
| 16 | ||
| 17 | class 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 Safronov | 38 | PRICES = {"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 | ||
| 44 | def commit(sha, date, message, author="a"): | |
| 45 | return ([sha, sha[:3], author, date, "subject"], message) | |
| 46 | ||
| 47 | ||
| 48 | def 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 | ||
| 57 | class 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 | ||
| 123 | class 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 Safronov | 147 | def test_line_chart_svg(self): |
ced0eafa 2026-11-04 Pavel Safronov | 148 | b = self.buckets() |
799905ae 2026-11-04 Pavel Safronov | 149 | 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 Safronov | 152 | 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 Safronov | 155 | 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 Safronov | 156 | self.assertEqual(svg.count('class="unk"'), 1) # only the first week has commits without data |
799905ae 2026-11-04 Pavel Safronov | 157 | 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 Safronov | 159 | 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 Safronov | 164 | 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("<b>x</b>", svg) | |
| 203 | ||
ced0eafa 2026-11-04 Pavel Safronov | 204 | |
| 205 | class 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 Safronov | 225 | class 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 Safronov | 227 | 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 Safronov | 230 | 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 Safronov | 235 | |
| 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 Safronov | 253 | |
| 254 | ||
| 255 | class UsageRendering(UsageFixtures, unittest.TestCase): | |
ced0eafa 2026-11-04 Pavel Safronov | 256 | 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("<b>x</b>", 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 Safronov | 306 | 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 Safronov | 308 | self.assertIn('id="p2"', svg) |
| 309 | self.assertIn("url(#p2)", svg) | |
| 310 | self.assertEqual(svg.count('class="hit"'), 2) | |
799905ae 2026-11-04 Pavel Safronov | 311 | self.assertEqual(svg.count("<circle"), 1) |
| 312 | ||
| 313 | class 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("<b>x</b>", 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 Safronov | 531 | |
392edf54 2026-11-04 Pavel Safronov | 532 | 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 Safronov | 586 | |
b447a9f2 2026-11-03 Pavel Safronov | 587 | if __name__ == "__main__": |
| 588 | unittest.main() |