gabriel / muse public
test_velocity_supercharge.py python
775 lines 33.7 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """Seven-tier tests for ``muse/cli/commands/velocity.py``.
2
3 Tiers
4 -----
5 Unit — _module_of; _bar; _WindowStats.net; _compute_predictions;
6 _print_table (empty, with modules, with predictions, truncated).
7 Integration — _VelocityJson TypedDict fields; -j alias; register() docstring;
8 run() docstring envelope fields.
9 End-to-end — --json emits schema_version/mode/exit_code/duration_ms;
10 -j alias; --predict -j; human output unchanged; empty repo.
11 Stress — 1 000 _module_of; 500 _bar; _print_table with 200 modules.
12 Data integrity — schema_version str; exit_code int; duration_ms float;
13 modules list; predictions list; window_size preserved.
14 Security — hostile address in predictions survives JSON; SQL injection
15 in since; very long module path; unicode addresses.
16 Performance — 1 000 _module_of under 200 ms; velocity JSON completes quickly.
17 """
18
19 from __future__ import annotations
20 from collections.abc import Mapping
21
22 import json
23 import os
24 import pathlib
25 import textwrap
26 import threading
27 import time
28 from typing import get_type_hints
29
30 import pytest
31
32 from tests.cli_test_helper import CliRunner, InvokeResult
33
34 runner = CliRunner()
35
36
37 # ──────────────────────────────────────────────────────────────────────────────
38 # Shared helpers
39 # ──────────────────────────────────────────────────────────────────────────────
40
41
42 def _make_window(**kw):
43 from muse.cli.commands.velocity import _WindowStats
44 w = _WindowStats()
45 for k, v in kw.items():
46 setattr(w, k, v)
47 return w
48
49
50 def _make_accumulator(current=None, prior=None, last_active_rank=-1, stagnant_commits=0):
51 from muse.cli.commands.velocity import _ModuleAccumulator, _WindowStats
52 acc = _ModuleAccumulator()
53 if current is not None:
54 acc.current = current
55 if prior is not None:
56 acc.prior = prior
57 acc.last_active_rank = last_active_rank
58 acc.stagnant_commits = stagnant_commits
59 return acc
60
61
62 def _make_sym_freq(frequency=3, last_rank=0, module="src/"):
63 from muse.cli.commands.velocity import _SymbolFreq
64 sf = _SymbolFreq(frequency=frequency, last_rank=last_rank, module=module)
65 return sf
66
67
68 def _commit(repo: pathlib.Path, files: Mapping[str, str], message: str) -> None:
69 for name, content in files.items():
70 path = repo / name
71 path.parent.mkdir(parents=True, exist_ok=True)
72 path.write_text(content, encoding="utf-8")
73 saved = os.getcwd()
74 try:
75 os.chdir(repo)
76 runner.invoke(None, ["code", "add", "."])
77 runner.invoke(None, ["commit", "-m", message])
78 finally:
79 os.chdir(saved)
80
81
82 def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult:
83 saved = os.getcwd()
84 try:
85 os.chdir(repo)
86 return runner.invoke(None, args)
87 finally:
88 os.chdir(saved)
89
90
91 @pytest.fixture()
92 def vel_repo(tmp_path: pathlib.Path) -> pathlib.Path:
93 """Minimal repo with two commits so velocity has commit history to walk."""
94 saved = os.getcwd()
95 try:
96 os.chdir(tmp_path)
97 runner.invoke(None, ["init"])
98 finally:
99 os.chdir(saved)
100
101 _commit(tmp_path, {
102 "src/calc.py": textwrap.dedent("""\
103 def add(a: int, b: int) -> int:
104 return a + b
105
106 def subtract(a: int, b: int) -> int:
107 return a - b
108 """),
109 "src/utils.py": textwrap.dedent("""\
110 def helper(x: str) -> str:
111 return x.upper()
112 """),
113 }, "feat: initial symbols")
114
115 _commit(tmp_path, {
116 "src/calc.py": textwrap.dedent("""\
117 def add(a: int, b: int) -> int:
118 return a + b
119
120 def subtract(a: int, b: int) -> int:
121 return a - b
122
123 def multiply(a: int, b: int) -> int:
124 return a * b
125 """),
126 "src/engine.py": textwrap.dedent("""\
127 def process(data: list) -> list:
128 return sorted(data)
129
130 def validate(item) -> bool:
131 return item is not None
132 """),
133 }, "feat: add multiply and engine module")
134
135 return tmp_path
136
137
138 # ──────────────────────────────────────────────────────────────────────────────
139 # Unit — _module_of
140 # ──────────────────────────────────────────────────────────────────────────────
141
142
143 class TestModuleOf:
144 def test_nested_path_returns_dir_with_slash(self) -> None:
145 from muse.cli.commands.velocity import _module_of
146 assert _module_of("muse/core/store.py") == "muse/core/"
147
148 def test_single_level_path(self) -> None:
149 from muse.cli.commands.velocity import _module_of
150 assert _module_of("tests/test_foo.py") == "tests/"
151
152 def test_root_file_returns_root_sentinel(self) -> None:
153 from muse.cli.commands.velocity import _module_of
154 assert _module_of("billing.py") == "(root)"
155
156 def test_windows_backslash_normalised(self) -> None:
157 from muse.cli.commands.velocity import _module_of
158 assert _module_of("muse\\core\\store.py") == "muse/core/"
159
160 def test_deeply_nested_path(self) -> None:
161 from muse.cli.commands.velocity import _module_of
162 assert _module_of("a/b/c/d/e.py") == "a/b/c/d/"
163
164 def test_returns_str(self) -> None:
165 from muse.cli.commands.velocity import _module_of
166 assert isinstance(_module_of("src/foo.py"), str)
167
168
169 # ──────────────────────────────────────────────────────────────────────────────
170 # Unit — _bar
171 # ──────────────────────────────────────────────────────────────────────────────
172
173
174 class TestBar:
175 def test_zero_max_returns_empty(self) -> None:
176 from muse.cli.commands.velocity import _bar
177 assert _bar(5, 0) == ""
178
179 def test_positive_net_returns_filled_bar(self) -> None:
180 from muse.cli.commands.velocity import _bar
181 result = _bar(10, 10)
182 assert "█" in result
183
184 def test_negative_net_includes_negative_label(self) -> None:
185 from muse.cli.commands.velocity import _bar
186 result = _bar(-5, 10)
187 assert "net negative" in result
188
189 def test_zero_net_returns_minimal_bar(self) -> None:
190 from muse.cli.commands.velocity import _bar
191 result = _bar(0, 10)
192 # Zero net with positive max: filled=0 → returns "▏"
193 assert result == "▏"
194
195 def test_full_bar_has_max_blocks(self) -> None:
196 from muse.cli.commands.velocity import _bar, _BAR_WIDTH
197 result = _bar(100, 100)
198 assert result.count("█") == _BAR_WIDTH
199
200 def test_partial_bar_proportional(self) -> None:
201 from muse.cli.commands.velocity import _bar
202 full = _bar(10, 10)
203 half = _bar(5, 10)
204 assert len(half) <= len(full)
205
206
207 # ──────────────────────────────────────────────────────────────────────────────
208 # Unit — _WindowStats
209 # ──────────────────────────────────────────────────────────────────────────────
210
211
212 class TestWindowStats:
213 def test_net_positive(self) -> None:
214 from muse.cli.commands.velocity import _WindowStats
215 w = _WindowStats(added=10, removed=3)
216 assert w.net == 7
217
218 def test_net_negative(self) -> None:
219 from muse.cli.commands.velocity import _WindowStats
220 w = _WindowStats(added=2, removed=8)
221 assert w.net == -6
222
223 def test_net_zero(self) -> None:
224 from muse.cli.commands.velocity import _WindowStats
225 w = _WindowStats(added=5, removed=5)
226 assert w.net == 0
227
228 def test_defaults_are_zero(self) -> None:
229 from muse.cli.commands.velocity import _WindowStats
230 w = _WindowStats()
231 assert w.added == 0
232 assert w.removed == 0
233 assert w.modified == 0
234 assert w.active_commits == 0
235 assert w.net == 0
236
237
238 # ──────────────────────────────────────────────────────────────────────────────
239 # Unit — _compute_predictions
240 # ──────────────────────────────────────────────────────────────────────────────
241
242
243 class TestComputePredictions:
244 def test_empty_symbol_freq_returns_empty(self) -> None:
245 from muse.cli.commands.velocity import _compute_predictions
246 result = _compute_predictions({}, {}, window_size=20, top_k=5)
247 assert result == []
248
249 def test_top_k_zero_returns_empty(self) -> None:
250 from muse.cli.commands.velocity import _compute_predictions
251 sym = {"src/foo.py::bar": _make_sym_freq(frequency=5)}
252 result = _compute_predictions(sym, {}, window_size=20, top_k=0)
253 assert result == []
254
255 def test_top_k_limits_results(self) -> None:
256 from muse.cli.commands.velocity import _compute_predictions
257 syms = {f"src/f{i}.py::fn": _make_sym_freq(frequency=i + 1)
258 for i in range(10)}
259 result = _compute_predictions(syms, {}, window_size=20, top_k=3)
260 assert len(result) == 3
261
262 def test_higher_frequency_ranks_first(self) -> None:
263 from muse.cli.commands.velocity import _compute_predictions
264 syms = {
265 "a.py::low": _make_sym_freq(frequency=1, last_rank=0),
266 "b.py::high": _make_sym_freq(frequency=10, last_rank=0),
267 }
268 result = _compute_predictions(syms, {}, window_size=20, top_k=2)
269 assert result[0]["address"] == "b.py::high"
270
271 def test_result_has_required_fields(self) -> None:
272 from muse.cli.commands.velocity import _compute_predictions
273 syms = {"src/foo.py::bar": _make_sym_freq(frequency=3)}
274 result = _compute_predictions(syms, {}, window_size=20, top_k=1)
275 assert len(result) == 1
276 out = result[0]
277 for field in ("address", "module", "score", "frequency", "last_commit_rank"):
278 assert field in out, f"missing: {field}"
279
280 def test_score_is_positive(self) -> None:
281 from muse.cli.commands.velocity import _compute_predictions
282 syms = {"src/foo.py::bar": _make_sym_freq(frequency=5, last_rank=1)}
283 result = _compute_predictions(syms, {}, window_size=20, top_k=1)
284 assert result[0]["score"] > 0.0
285
286 def test_recent_symbol_scores_higher_than_old(self) -> None:
287 from muse.cli.commands.velocity import _compute_predictions
288 syms = {
289 "a.py::recent": _make_sym_freq(frequency=2, last_rank=0),
290 "b.py::old": _make_sym_freq(frequency=2, last_rank=15),
291 }
292 result = _compute_predictions(syms, {}, window_size=20, top_k=2)
293 assert result[0]["address"] == "a.py::recent"
294
295 def test_module_velocity_boost_applied(self) -> None:
296 from muse.cli.commands.velocity import _compute_predictions, _ModuleAccumulator, _WindowStats
297 fast_mod = _ModuleAccumulator()
298 fast_mod.current = _WindowStats(added=10, removed=0)
299 slow_mod = _ModuleAccumulator()
300 slow_mod.current = _WindowStats(added=0, removed=0)
301 modules = {"fast/": fast_mod, "slow/": slow_mod}
302 syms = {
303 "fast/a.py::fn": _make_sym_freq(frequency=3, last_rank=0, module="fast/"),
304 "slow/b.py::fn": _make_sym_freq(frequency=3, last_rank=0, module="slow/"),
305 }
306 result = _compute_predictions(syms, modules, window_size=20, top_k=2)
307 # fast/ module gets velocity boost
308 assert result[0]["module"] == "fast/"
309
310
311 # ──────────────────────────────────────────────────────────────────────────────
312 # Unit — _print_table
313 # ──────────────────────────────────────────────────────────────────────────────
314
315
316 class TestPrintTable:
317 def test_empty_ranked_prints_no_changes_message(self, capsys) -> None:
318 from muse.cli.commands.velocity import _print_table
319 _print_table([], [], ref="dev", commits_analysed=10,
320 window_size=5, truncated=False, since=None)
321 assert "no modules" in capsys.readouterr().out.lower()
322
323 def test_header_shows_ref(self, capsys) -> None:
324 from muse.cli.commands.velocity import _print_table
325 _print_table([], [], ref="my-branch", commits_analysed=0,
326 window_size=5, truncated=False, since=None)
327 assert "my-branch" in capsys.readouterr().out
328
329 def test_truncated_shows_warning(self, capsys) -> None:
330 from muse.cli.commands.velocity import _print_table
331 _print_table([], [], ref="dev", commits_analysed=100,
332 window_size=5, truncated=True, since=None)
333 assert "truncated" in capsys.readouterr().out
334
335 def test_since_shown_in_scope(self, capsys) -> None:
336 from muse.cli.commands.velocity import _print_table
337 _print_table([], [], ref="dev", commits_analysed=10,
338 window_size=5, truncated=False, since="v1.0")
339 assert "v1.0" in capsys.readouterr().out
340
341 def test_module_row_shown(self, capsys) -> None:
342 from muse.cli.commands.velocity import _print_table
343 acc = _make_accumulator(
344 current=_make_window(added=5, removed=1, modified=3),
345 )
346 _print_table([("src/core/", acc)], [], ref="dev",
347 commits_analysed=20, window_size=10,
348 truncated=False, since=None)
349 assert "src/core/" in capsys.readouterr().out
350
351 def test_stagnant_module_shows_note(self, capsys) -> None:
352 from muse.cli.commands.velocity import _print_table
353 acc = _make_accumulator(stagnant_commits=8)
354 _print_table([("docs/", acc)], [], ref="dev",
355 commits_analysed=20, window_size=10,
356 truncated=False, since=None)
357 assert "stagnant" in capsys.readouterr().out
358
359 def test_predictions_shown(self, capsys) -> None:
360 from muse.cli.commands.velocity import _print_table, _PredictionOut
361 pred = _PredictionOut(address="src/core/store.py::read",
362 module="src/core/", score=0.91,
363 frequency=5, last_commit_rank=0)
364 acc = _make_accumulator(current=_make_window(added=1))
365 _print_table([("src/core/", acc)], [pred], ref="dev", commits_analysed=20,
366 window_size=10, truncated=False, since=None)
367 assert "src/core/store.py::read" in capsys.readouterr().out
368
369 def test_acceleration_leader_shown(self, capsys) -> None:
370 from muse.cli.commands.velocity import _print_table
371 current = _make_window(added=10, removed=0, modified=5)
372 prior = _make_window(added=2, removed=0, modified=1)
373 acc = _make_accumulator(current=current, prior=prior)
374 _print_table([("src/hot/", acc)], [], ref="dev",
375 commits_analysed=40, window_size=20,
376 truncated=False, since=None)
377 out = capsys.readouterr().out
378 assert "Acceleration" in out or "src/hot/" in out
379
380
381 # ──────────────────────────────────────────────────────────────────────────────
382 # Integration — TypedDict, alias, docstrings
383 # ──────────────────────────────────────────────────────────────────────────────
384
385
386 class TestTypedDict:
387 def test_velocity_json_has_schema_version(self) -> None:
388 from muse.cli.commands.velocity import _VelocityJson
389 assert "schema" in get_type_hints(_VelocityJson)
390
391 def test_velocity_json_has_exit_code(self) -> None:
392 from muse.cli.commands.velocity import _VelocityJson
393 assert "exit_code" in get_type_hints(_VelocityJson)
394
395 def test_velocity_json_has_duration_ms(self) -> None:
396 from muse.cli.commands.velocity import _VelocityJson
397 assert "duration_ms" in get_type_hints(_VelocityJson)
398
399 def test_velocity_json_has_mode(self) -> None:
400 from muse.cli.commands.velocity import _VelocityJson
401 assert "mode" in get_type_hints(_VelocityJson)
402
403 def test_velocity_json_has_modules(self) -> None:
404 from muse.cli.commands.velocity import _VelocityJson
405 assert "modules" in get_type_hints(_VelocityJson)
406
407 def test_velocity_json_has_predictions(self) -> None:
408 from muse.cli.commands.velocity import _VelocityJson
409 assert "predictions" in get_type_hints(_VelocityJson)
410
411 def test_velocity_json_has_ref(self) -> None:
412 from muse.cli.commands.velocity import _VelocityJson
413 assert "ref" in get_type_hints(_VelocityJson)
414
415 def test_velocity_json_has_window_size(self) -> None:
416 from muse.cli.commands.velocity import _VelocityJson
417 assert "window_size" in get_type_hints(_VelocityJson)
418
419 def test_velocity_json_has_truncated(self) -> None:
420 from muse.cli.commands.velocity import _VelocityJson
421 assert "truncated" in get_type_hints(_VelocityJson)
422
423
424 class TestAliasRegistration:
425 def test_j_alias_registered(self) -> None:
426 from muse.cli.commands.velocity import register
427 import argparse
428 p = argparse.ArgumentParser()
429 sub = p.add_subparsers()
430 register(sub)
431 ns = p.parse_args(["velocity", "-j"])
432 assert ns.json_out is True
433
434 def test_json_long_form_works(self) -> None:
435 from muse.cli.commands.velocity import register
436 import argparse
437 p = argparse.ArgumentParser()
438 sub = p.add_subparsers()
439 register(sub)
440 ns = p.parse_args(["velocity", "--json"])
441 assert ns.json_out is True
442
443 def test_j_and_predict_parse_together(self) -> None:
444 from muse.cli.commands.velocity import register
445 import argparse
446 p = argparse.ArgumentParser()
447 sub = p.add_subparsers()
448 register(sub)
449 ns = p.parse_args(["velocity", "--predict", "5", "-j"])
450 assert ns.json_out is True
451 assert ns.predict == 5
452
453
454 class TestDocstrings:
455 def test_register_mentions_j_alias(self) -> None:
456 from muse.cli.commands.velocity import register
457 doc = register.__doc__ or ""
458 assert "-j" in doc or "--json" in doc
459
460 def test_run_mentions_schema_version(self) -> None:
461 from muse.cli.commands.velocity import run
462 assert "schema" in (run.__doc__ or "")
463
464 def test_run_mentions_exit_code(self) -> None:
465 from muse.cli.commands.velocity import run
466 assert "exit_code" in (run.__doc__ or "")
467
468 def test_run_mentions_duration_ms(self) -> None:
469 from muse.cli.commands.velocity import run
470 assert "duration_ms" in (run.__doc__ or "")
471
472 def test_run_mentions_mode(self) -> None:
473 from muse.cli.commands.velocity import run
474 assert "mode" in (run.__doc__ or "")
475
476
477 # ──────────────────────────────────────────────────────────────────────────────
478 # End-to-end
479 # ──────────────────────────────────────────────────────────────────────────────
480
481
482 class TestEndToEnd:
483 def test_json_exits_zero(self, vel_repo) -> None:
484 r = _invoke(vel_repo, ["code", "velocity", "--json"])
485 assert r.exit_code == 0
486
487 def test_json_emits_schema_version(self, vel_repo) -> None:
488 r = _invoke(vel_repo, ["code", "velocity", "--json"])
489 assert r.exit_code == 0
490 assert "schema" in json.loads(r.output)
491
492 def test_json_emits_mode(self, vel_repo) -> None:
493 r = _invoke(vel_repo, ["code", "velocity", "--json"])
494 assert r.exit_code == 0
495 assert json.loads(r.output)["mode"] == "velocity"
496
497 def test_json_emits_exit_code(self, vel_repo) -> None:
498 r = _invoke(vel_repo, ["code", "velocity", "--json"])
499 assert r.exit_code == 0
500 assert isinstance(json.loads(r.output)["exit_code"], int)
501
502 def test_json_emits_duration_ms(self, vel_repo) -> None:
503 r = _invoke(vel_repo, ["code", "velocity", "--json"])
504 assert r.exit_code == 0
505 d = json.loads(r.output)
506 assert isinstance(d["duration_ms"], float)
507 assert d["duration_ms"] >= 0.0
508
509 def test_json_emits_modules_list(self, vel_repo) -> None:
510 r = _invoke(vel_repo, ["code", "velocity", "--json"])
511 assert r.exit_code == 0
512 assert isinstance(json.loads(r.output)["modules"], list)
513
514 def test_json_emits_predictions_list(self, vel_repo) -> None:
515 r = _invoke(vel_repo, ["code", "velocity", "--json"])
516 assert r.exit_code == 0
517 assert isinstance(json.loads(r.output)["predictions"], list)
518
519 def test_json_emits_window_size(self, vel_repo) -> None:
520 r = _invoke(vel_repo, ["code", "velocity", "--window", "5", "--json"])
521 assert r.exit_code == 0
522 d = json.loads(r.output)
523 assert d["window_size"] == 5
524
525 def test_json_emits_ref(self, vel_repo) -> None:
526 r = _invoke(vel_repo, ["code", "velocity", "--json"])
527 assert r.exit_code == 0
528 assert isinstance(json.loads(r.output)["ref"], str)
529
530 def test_json_emits_truncated(self, vel_repo) -> None:
531 r = _invoke(vel_repo, ["code", "velocity", "--json"])
532 assert r.exit_code == 0
533 assert isinstance(json.loads(r.output)["truncated"], bool)
534
535 def test_j_alias_produces_json(self, vel_repo) -> None:
536 r = _invoke(vel_repo, ["code", "velocity", "-j"])
537 assert r.exit_code == 0
538 d = json.loads(r.output)
539 assert d["mode"] == "velocity"
540
541 def test_predict_j_includes_predictions(self, vel_repo) -> None:
542 r = _invoke(vel_repo, ["code", "velocity", "--predict", "3", "-j"])
543 assert r.exit_code == 0
544 d = json.loads(r.output)
545 assert "predictions" in d
546 # predictions may be empty if no current-window hits, but key must exist
547 assert isinstance(d["predictions"], list)
548
549 def test_human_output_still_works(self, vel_repo) -> None:
550 r = _invoke(vel_repo, ["code", "velocity"])
551 assert r.exit_code == 0
552 assert "Symbol velocity" in r.output
553
554 def test_schema_version_matches_muse_version(self, vel_repo) -> None:
555 from muse import __version__
556 r = _invoke(vel_repo, ["code", "velocity", "--json"])
557 assert r.exit_code == 0
558 assert isinstance(json.loads(r.output)["schema"], int)
559
560
561 # ──────────────────────────────────────────────────────────────────────────────
562 # Stress
563 # ──────────────────────────────────────────────────────────────────────────────
564
565
566 class TestStress:
567 def test_1000_module_of(self) -> None:
568 from muse.cli.commands.velocity import _module_of
569 for i in range(1_000):
570 r = _module_of(f"src/pkg_{i}/file_{i}.py")
571 assert r == f"src/pkg_{i}/"
572
573 def test_500_bar_calls(self) -> None:
574 from muse.cli.commands.velocity import _bar
575 for i in range(500):
576 r = _bar(i % 20, 20)
577 assert isinstance(r, str)
578
579 def test_print_table_200_modules(self, capsys) -> None:
580 from muse.cli.commands.velocity import _print_table
581 ranked = [
582 (f"mod_{i}/", _make_accumulator(
583 current=_make_window(added=i, removed=0, modified=i),
584 ))
585 for i in range(200)
586 ]
587 _print_table(ranked, [], ref="dev", commits_analysed=400,
588 window_size=20, truncated=False, since=None)
589 out = capsys.readouterr().out
590 assert "mod_0/" in out
591
592 def test_concurrent_module_of(self) -> None:
593 from muse.cli.commands.velocity import _module_of
594 results: list[str] = []
595 lock = threading.Lock()
596
597 def _run() -> None:
598 r = _module_of("src/core/store.py")
599 with lock:
600 results.append(r)
601
602 threads = [threading.Thread(target=_run) for _ in range(50)]
603 for t in threads: t.start()
604 for t in threads: t.join()
605 assert all(r == "src/core/" for r in results)
606 assert len(results) == 50
607
608
609 # ──────────────────────────────────────────────────────────────────────────────
610 # Data integrity
611 # ──────────────────────────────────────────────────────────────────────────────
612
613
614 class TestDataIntegrity:
615 def test_schema_version_is_str(self, vel_repo) -> None:
616 r = _invoke(vel_repo, ["code", "velocity", "--json"])
617 assert isinstance(json.loads(r.output)["schema"], int)
618
619 def test_schema_version_nonempty(self, vel_repo) -> None:
620 r = _invoke(vel_repo, ["code", "velocity", "--json"])
621 assert json.loads(r.output)["schema"] > 0
622
623 def test_exit_code_is_int(self, vel_repo) -> None:
624 r = _invoke(vel_repo, ["code", "velocity", "--json"])
625 assert isinstance(json.loads(r.output)["exit_code"], int)
626
627 def test_duration_ms_is_float(self, vel_repo) -> None:
628 r = _invoke(vel_repo, ["code", "velocity", "--json"])
629 d = json.loads(r.output)
630 assert isinstance(d["duration_ms"], float)
631
632 def test_duration_ms_non_negative(self, vel_repo) -> None:
633 r = _invoke(vel_repo, ["code", "velocity", "--json"])
634 assert json.loads(r.output)["duration_ms"] >= 0.0
635
636 def test_window_size_preserved(self, vel_repo) -> None:
637 r = _invoke(vel_repo, ["code", "velocity", "--window", "7", "--json"])
638 assert json.loads(r.output)["window_size"] == 7
639
640 def test_modules_entries_have_required_fields(self, vel_repo) -> None:
641 r = _invoke(vel_repo, ["code", "velocity", "--json"])
642 d = json.loads(r.output)
643 for m in d["modules"]:
644 for field in ("module", "current", "prior", "acceleration", "stagnant_commits"):
645 assert field in m, f"missing: {field}"
646
647 def test_json_serialisable(self, vel_repo) -> None:
648 r = _invoke(vel_repo, ["code", "velocity", "--json"])
649 json.loads(r.output) # must not raise
650
651 def test_mode_is_velocity(self, vel_repo) -> None:
652 r = _invoke(vel_repo, ["code", "velocity", "--json"])
653 assert json.loads(r.output)["mode"] == "velocity"
654
655 def test_predictions_entries_have_required_fields(self, vel_repo) -> None:
656 r = _invoke(vel_repo, ["code", "velocity", "--predict", "5", "--json"])
657 d = json.loads(r.output)
658 for p in d["predictions"]:
659 for field in ("address", "score"):
660 assert field in p, f"missing: {field}"
661
662
663 # ──────────────────────────────────────────────────────────────────────────────
664 # Security
665 # ──────────────────────────────────────────────────────────────────────────────
666
667
668 class TestSecurity:
669 def test_hostile_address_in_predictions_survives_json(self) -> None:
670 from muse.cli.commands.velocity import _compute_predictions, _SymbolFreq
671 evil = '"; DROP TABLE commits; --'
672 syms = {evil: _make_sym_freq(frequency=5)}
673 result = _compute_predictions(syms, {}, window_size=20, top_k=1)
674 assert len(result) == 1
675 serialised = json.dumps(result[0])
676 assert json.loads(serialised)["address"] == evil
677
678 def test_xss_in_module_name_does_not_crash(self, capsys) -> None:
679 from muse.cli.commands.velocity import _print_table
680 acc = _make_accumulator(current=_make_window(added=1))
681 _print_table([("<script>alert(1)</script>/", acc)], [],
682 ref="dev", commits_analysed=5,
683 window_size=2, truncated=False, since=None)
684 assert "<script>" in capsys.readouterr().out
685
686 def test_very_long_module_path_does_not_crash(self, capsys) -> None:
687 from muse.cli.commands.velocity import _print_table
688 long_mod = "x" * 500 + "/"
689 acc = _make_accumulator(current=_make_window(added=2))
690 _print_table([(long_mod, acc)], [],
691 ref="dev", commits_analysed=5,
692 window_size=2, truncated=False, since=None)
693 assert capsys.readouterr().out # no crash
694
695 def test_unicode_in_prediction_address(self) -> None:
696 from muse.cli.commands.velocity import _compute_predictions
697 addr = "src/音符.py::計算"
698 syms = {addr: _make_sym_freq(frequency=3)}
699 result = _compute_predictions(syms, {}, window_size=20, top_k=1)
700 serialised = json.dumps(result[0], ensure_ascii=False)
701 assert json.loads(serialised)["address"] == addr
702
703 def test_null_byte_in_module_survives(self) -> None:
704 from muse.cli.commands.velocity import _module_of
705 # null byte in path — should not crash
706 result = _module_of("src\x00evil/file.py")
707 assert isinstance(result, str)
708
709 def test_very_large_frequency_does_not_crash(self) -> None:
710 from muse.cli.commands.velocity import _compute_predictions
711 syms = {"src/foo.py::bar": _make_sym_freq(frequency=10**9)}
712 result = _compute_predictions(syms, {}, window_size=20, top_k=1)
713 assert len(result) == 1
714 assert result[0]["score"] > 0
715
716
717 # ──────────────────────────────────────────────────────────────────────────────
718 # Performance
719 # ──────────────────────────────────────────────────────────────────────────────
720
721
722 class TestPerformance:
723 def test_1000_module_of_under_200ms(self) -> None:
724 from muse.cli.commands.velocity import _module_of
725 start = time.perf_counter()
726 for i in range(1_000):
727 _module_of(f"muse/core/sub_{i}/file.py")
728 elapsed = time.perf_counter() - start
729 assert elapsed < 0.2, f"1 000 _module_of took {elapsed:.3f}s"
730
731 def test_500_bar_under_100ms(self) -> None:
732 from muse.cli.commands.velocity import _bar
733 start = time.perf_counter()
734 for i in range(500):
735 _bar(i, 500)
736 elapsed = time.perf_counter() - start
737 assert elapsed < 0.1, f"500 _bar took {elapsed:.3f}s"
738
739 def test_velocity_json_completes_quickly(self, vel_repo) -> None:
740 start = time.perf_counter()
741 r = _invoke(vel_repo, ["code", "velocity", "--json"])
742 elapsed = time.perf_counter() - start
743 assert r.exit_code == 0
744 assert elapsed < 15.0, f"velocity --json took {elapsed:.2f}s"
745
746 def test_duration_ms_positive(self, vel_repo) -> None:
747 r = _invoke(vel_repo, ["code", "velocity", "--json"])
748 assert json.loads(r.output)["duration_ms"] >= 0.0
749
750
751 # ──────────────────────────────────────────────────────────────────────────────
752 # Flag registration
753 # ──────────────────────────────────────────────────────────────────────────────
754
755
756 class TestRegisterFlags:
757 def _parse(self, *args: str):
758 import argparse
759 from muse.cli.commands.velocity import register
760 p = argparse.ArgumentParser()
761 sub = p.add_subparsers()
762 register(sub)
763 return p.parse_args(["velocity", *args])
764
765 def test_default_json_out_is_false(self) -> None:
766 ns = self._parse()
767 assert ns.json_out is False
768
769 def test_json_flag_sets_json_out(self) -> None:
770 ns = self._parse("--json")
771 assert ns.json_out is True
772
773 def test_j_shorthand_sets_json_out(self) -> None:
774 ns = self._parse("-j")
775 assert ns.json_out is True
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 137 days ago