gabriel / muse public

test_cmd_check_ref_format.py file-level

at sha256:8 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 πŸ’₯ blast risk
sha256:4 Merge branch 'dev' into main · gabriel · Jun 17, 2026
1 """Tests for muse check-ref-format.
2
3 Coverage tiers
4 --------------
5 Unit β€” _CheckResult schema, _CheckRefFormatResult schema,
6 _RulesDict schema, _RULES content correctness
7 Integration β€” valid names (simple, namespaced, hierarchical, edge-length),
8 invalid names (each rule: leading-dot, trailing-dot, consecutive-dot,
9 leading-slash, trailing-slash, consecutive-slash, null-byte, backslash,
10 tab, CR, LF, empty, too-long),
11 mixed validity, all-valid exit 0, any-invalid exit 1,
12 --quiet mode, --format text, --json shorthand, --rules (json+text),
13 --stdin (read, blanks/comments skipped, combined, empty errors),
14 valid_count / invalid_count fields, error output to stderr
15 Security β€” ANSI in name sanitized in text output, ANSI in --rules safe,
16 format error to stderr, no traceback on bad format,
17 null-byte name rejected cleanly
18 Stress β€” 500 valid names, 500 invalid names, 200 sequential calls,
19 255-char max-length name, 256-char over-length name,
20 1000-name stdin batch
21 """
22
23 from __future__ import annotations
24
25 import json
26 import pathlib
27
28 import pytest
29 from tests.cli_test_helper import CliRunner, InvokeResult
30
31 from muse.cli.commands.check_ref_format import (
32 _RULES,
33 _CheckRefFormatJson as _CheckRefFormatResult,
34 _CheckResult,
35 _RulesDict,
36 )
37
38 cli = None # argparse-based CLI; CliRunner ignores this arg
39 runner = CliRunner()
40
41
42 # ---------------------------------------------------------------------------
43 # Helper β€” check-ref-format needs no repo (pure CPU)
44 # ---------------------------------------------------------------------------
45
46
47 def _crf(*args: str, stdin: str | None = None) -> InvokeResult:
48 """Invoke check-ref-format with no MUSE_REPO_ROOT constraint."""
49 return runner.invoke(cli, ["check-ref-format", *args], input=stdin)
50
51
52 # ---------------------------------------------------------------------------
53 # Unit β€” schemas and constants
54 # ---------------------------------------------------------------------------
55
56
57 class TestSchemas:
58 def test_check_result_fields(self) -> None:
59 keys = _CheckResult.__annotations__
60 assert "name" in keys
61 assert "valid" in keys
62 assert "error" in keys
63
64 def test_check_ref_format_result_fields(self) -> None:
65 keys = _CheckRefFormatResult.__annotations__
66 assert "results" in keys
67 assert "all_valid" in keys
68 assert "valid_count" in keys
69 assert "invalid_count" in keys
70
71 def test_rules_dict_fields(self) -> None:
72 keys = _RulesDict.__annotations__
73 assert "max_length" in keys
74 assert "forbidden_chars" in keys
75 assert "forbidden_patterns" in keys
76 assert "notes" in keys
77
78 def test_check_ref_format_result_has_elapsed(self) -> None:
79 assert "duration_ms" in _CheckRefFormatResult.__annotations__
80
81 def test_check_ref_format_result_has_exit_code(self) -> None:
82 assert "exit_code" in _CheckRefFormatResult.__annotations__
83
84 def test_rules_max_length(self) -> None:
85 assert _RULES["max_length"] == 255
86
87 def test_rules_forbidden_chars_includes_c0_controls(self) -> None:
88 """Null byte is covered by the C0 controls group."""
89 forbidden_chars = _RULES["forbidden_chars"]
90 # Either the literal null byte or a descriptive C0 group string is acceptable.
91 has_null = "\x00" in forbidden_chars
92 has_c0_group = any("C0" in s or "0x00" in s for s in forbidden_chars)
93 assert has_null or has_c0_group, "null byte must be covered in forbidden_chars"
94
95 def test_rules_forbidden_chars_includes_backslash(self) -> None:
96 assert "\\" in _RULES["forbidden_chars"]
97
98 def test_rules_forbidden_patterns_not_empty(self) -> None:
99 assert len(_RULES["forbidden_patterns"]) >= 4
100
101 def test_rules_notes_mentions_slash_ok(self) -> None:
102 assert "/" in _RULES["notes"] or "slash" in _RULES["notes"].lower()
103
104
105 # ---------------------------------------------------------------------------
106 # Integration β€” valid names
107 # ---------------------------------------------------------------------------
108
109
110 class TestValidNames:
111 def test_simple_name(self) -> None:
112 r = _crf("--json", "main")
113 assert r.exit_code == 0
114 data = json.loads(r.output)
115 assert data["all_valid"] is True
116 assert data["results"][0]["valid"] is True
117 assert data["results"][0]["error"] is None
118
119 def test_namespaced_name(self) -> None:
120 r = _crf("--json", "feat/my-branch")
121 assert r.exit_code == 0
122 data = json.loads(r.output)
123 assert data["all_valid"] is True
124
125 def test_deeply_hierarchical_name(self) -> None:
126 r = _crf("--json", "team/feat/PROJ-42/wip")
127 assert r.exit_code == 0
128 assert json.loads(r.output)["all_valid"] is True
129
130 def test_name_with_numbers(self) -> None:
131 r = _crf("release-2026-03-27")
132 assert r.exit_code == 0
133
134 def test_single_char_name(self) -> None:
135 r = _crf("--json", "x")
136 assert r.exit_code == 0
137
138 def test_255_char_name_valid(self) -> None:
139 name = "a" * 255
140 r = _crf("--json", name)
141 assert r.exit_code == 0
142 assert json.loads(r.output)["all_valid"] is True
143
144 def test_all_valid_exits_zero(self) -> None:
145 r = _crf("--json", "feat/a", "fix/b", "dev", "main")
146 assert r.exit_code == 0
147 data = json.loads(r.output)
148 assert data["all_valid"] is True
149 assert data["valid_count"] == 4
150 assert data["invalid_count"] == 0
151
152
153 # ---------------------------------------------------------------------------
154 # Integration β€” invalid names (each rule)
155 # ---------------------------------------------------------------------------
156
157
158 class TestInvalidNames:
159 def test_consecutive_dots(self) -> None:
160 r = _crf("--json", "bad..name")
161 assert r.exit_code != 0
162 data = json.loads(r.output)
163 assert data["all_valid"] is False
164 assert data["results"][0]["valid"] is False
165 assert data["results"][0]["error"] is not None
166
167 def test_leading_dot(self) -> None:
168 r = _crf("--json", ".hidden")
169 assert r.exit_code != 0
170 assert json.loads(r.output)["all_valid"] is False
171
172 def test_trailing_dot(self) -> None:
173 r = _crf("--json", "trailing.")
174 assert r.exit_code != 0
175 assert json.loads(r.output)["all_valid"] is False
176
177 def test_leading_slash(self) -> None:
178 r = _crf("/bad")
179 assert r.exit_code != 0
180
181 def test_trailing_slash(self) -> None:
182 r = _crf("bad/")
183 assert r.exit_code != 0
184
185 def test_consecutive_slashes(self) -> None:
186 r = _crf("bad//name")
187 assert r.exit_code != 0
188
189 def test_null_byte(self) -> None:
190 r = _crf("bad\x00name")
191 assert r.exit_code != 0
192
193 def test_backslash(self) -> None:
194 r = _crf("bad\\name")
195 assert r.exit_code != 0
196
197 def test_tab_character(self) -> None:
198 r = _crf("bad\tname")
199 assert r.exit_code != 0
200
201 def test_carriage_return(self) -> None:
202 r = _crf("bad\rname")
203 assert r.exit_code != 0
204
205 def test_newline(self) -> None:
206 r = _crf("bad\nname")
207 assert r.exit_code != 0
208
209 def test_empty_name(self) -> None:
210 r = _crf("--json", "")
211 assert r.exit_code != 0
212
213 def test_256_char_name_too_long(self) -> None:
214 name = "a" * 256
215 r = _crf("--json", name)
216 assert r.exit_code != 0
217 assert json.loads(r.output)["all_valid"] is False
218
219 def test_any_invalid_exits_nonzero(self) -> None:
220 r = _crf("--json", "good", "bad..name")
221 assert r.exit_code != 0
222
223 def test_invalid_count_correct(self) -> None:
224 r = _crf("--json", "good", "bad..name", ".also-bad")
225 data = json.loads(r.output)
226 assert data["valid_count"] == 1
227 assert data["invalid_count"] == 2
228
229
230 # ---------------------------------------------------------------------------
231 # Integration β€” valid_count / invalid_count fields
232 # ---------------------------------------------------------------------------
233
234
235 class TestCountFields:
236 def test_all_valid_counts(self) -> None:
237 r = _crf("--json", "a", "b", "c")
238 data = json.loads(r.output)
239 assert data["valid_count"] == 3
240 assert data["invalid_count"] == 0
241
242 def test_all_invalid_counts(self) -> None:
243 r = _crf("--json", "..a", "..b")
244 data = json.loads(r.output)
245 assert data["valid_count"] == 0
246 assert data["invalid_count"] == 2
247
248 def test_mixed_counts(self) -> None:
249 r = _crf("--json", "good", "..bad", "also-good", ".also-bad")
250 data = json.loads(r.output)
251 assert data["valid_count"] == 2
252 assert data["invalid_count"] == 2
253
254
255 # ---------------------------------------------------------------------------
256 # Integration β€” --quiet mode
257 # ---------------------------------------------------------------------------
258
259
260 class TestQuietMode:
261 def test_quiet_valid_exits_zero_no_output(self) -> None:
262 r = _crf("--quiet", "main")
263 assert r.exit_code == 0
264 assert r.output.strip() == ""
265
266 def test_quiet_invalid_exits_nonzero_no_output(self) -> None:
267 r = _crf("-q", "bad..name")
268 assert r.exit_code != 0
269 assert r.output.strip() == ""
270
271 def test_quiet_mixed_exits_nonzero(self) -> None:
272 r = _crf("--quiet", "good", "bad..name")
273 assert r.exit_code != 0
274
275
276 # ---------------------------------------------------------------------------
277 # Integration β€” text output
278 # ---------------------------------------------------------------------------
279
280
281 class TestTextOutput:
282 def test_valid_shows_ok(self) -> None:
283 r = _crf("main")
284 assert r.exit_code == 0
285 assert "ok" in r.output
286
287 def test_invalid_shows_fail(self) -> None:
288 r = _crf("bad..name")
289 assert r.exit_code != 0
290 assert "FAIL" in r.output
291
292 def test_mixed_shows_both(self) -> None:
293 r = _crf("good", "bad..name")
294 assert r.exit_code != 0
295 assert "ok" in r.output
296 assert "FAIL" in r.output
297
298 def test_json_shorthand_alias(self) -> None:
299 r = _crf("--json", "main")
300 assert r.exit_code == 0
301 data = json.loads(r.output)
302 assert "results" in data
303
304
305 # ---------------------------------------------------------------------------
306 # Integration β€” --rules
307 # ---------------------------------------------------------------------------
308
309
310 class TestRules:
311 def test_rules_json_output(self) -> None:
312 r = _crf("--json", "--rules")
313 assert r.exit_code == 0
314 data = json.loads(r.output)
315 assert "max_length" in data
316 assert "forbidden_chars" in data
317 assert "forbidden_patterns" in data
318 assert "notes" in data
319
320 def test_rules_max_length_is_255(self) -> None:
321 r = _crf("--json", "--rules")
322 data = json.loads(r.output)
323 assert data["max_length"] == 255
324
325 def test_rules_text_format(self) -> None:
326 r = _crf("--rules")
327 assert r.exit_code == 0
328 assert "max_length" in r.output
329 assert "forbidden" in r.output.lower()
330
331 def test_rules_needs_no_names(self) -> None:
332 """--rules exits cleanly with no name arguments."""
333 r = _crf("--json", "--rules")
334 assert r.exit_code == 0
335
336 def test_rules_json_is_parseable(self) -> None:
337 r = _crf("--rules", "--json")
338 assert r.exit_code == 0
339 data = json.loads(r.output)
340 assert isinstance(data["forbidden_chars"], list)
341 assert isinstance(data["forbidden_patterns"], list)
342
343
344 # ---------------------------------------------------------------------------
345 # Integration β€” --stdin
346 # ---------------------------------------------------------------------------
347
348
349 class TestStdinMode:
350 def test_stdin_reads_names(self) -> None:
351 r = _crf("--json", "--stdin", stdin="main\ndev\n")
352 assert r.exit_code == 0
353 data = json.loads(r.output)
354 assert data["valid_count"] == 2
355
356 def test_stdin_skips_blank_lines(self) -> None:
357 r = _crf("--json", "--stdin", stdin="\nmain\n\ndev\n\n")
358 assert r.exit_code == 0
359 data = json.loads(r.output)
360 assert len(data["results"]) == 2
361
362 def test_stdin_skips_comments(self) -> None:
363 r = _crf("--json", "--stdin", stdin="# a comment\nmain\n")
364 assert r.exit_code == 0
365 data = json.loads(r.output)
366 assert len(data["results"]) == 1
367
368 def test_stdin_combined_with_positional(self) -> None:
369 r = _crf("--json", "main", "--stdin", stdin="dev\n")
370 assert r.exit_code == 0
371 data = json.loads(r.output)
372 assert len(data["results"]) == 2
373
374 def test_stdin_invalid_name_from_stdin(self) -> None:
375 r = _crf("--json", "--stdin", stdin="bad..name\n")
376 assert r.exit_code != 0
377 data = json.loads(r.output)
378 assert data["all_valid"] is False
379
380 def test_stdin_empty_with_no_positional_errors(self) -> None:
381 r = _crf("--stdin", stdin="")
382 assert r.exit_code != 0
383 assert r.stdout_bytes == b""
384
385 def test_stdin_only_comments_errors(self) -> None:
386 r = _crf("--stdin", stdin="# comment only\n")
387 assert r.exit_code != 0
388 assert r.stdout_bytes == b""
389
390
391 # ---------------------------------------------------------------------------
392 # Security
393 # ---------------------------------------------------------------------------
394
395
396 class TestSecurity:
397 def test_ansi_in_name_stripped_text_output(self) -> None:
398 """ANSI escape in a branch name must not appear raw in text output."""
399 ansi_name = "\x1b[31mbadname\x1b[0m"
400 r = _crf(ansi_name)
401 assert "\x1b" not in r.output
402
403 def test_ansi_in_error_message_stripped(self) -> None:
404 """If the error message echoes the name, it must be sanitized."""
405 ansi_name = "\x1b[31m.leading\x1b[0m"
406 r = _crf(ansi_name)
407 assert "\x1b" not in r.output
408
409 def test_no_args_no_traceback(self) -> None:
410 r = _crf()
411 assert "Traceback" not in r.output
412 assert "Traceback" not in r.stderr
413
414 def test_null_byte_name_rejected_cleanly(self) -> None:
415 r = _crf("bad\x00name")
416 assert r.exit_code != 0
417 assert "Traceback" not in r.output
418 assert "Traceback" not in r.stderr
419
420 def test_no_args_error_to_stderr(self) -> None:
421 r = _crf()
422 assert r.exit_code != 0
423 assert r.stdout_bytes == b""
424
425 def test_rules_json_no_ansi(self) -> None:
426 r = _crf("--rules")
427 assert "\x1b" not in r.output
428
429
430 # ---------------------------------------------------------------------------
431 # Stress
432 # ---------------------------------------------------------------------------
433
434
435 class TestStress:
436 def test_500_valid_names(self) -> None:
437 names = [f"feat/task-{i:04d}" for i in range(500)]
438 r = _crf("--json", *names)
439 assert r.exit_code == 0
440 data = json.loads(r.output)
441 assert data["valid_count"] == 500
442 assert data["invalid_count"] == 0
443
444 def test_500_invalid_names(self) -> None:
445 names = [f"bad..{i}" for i in range(500)]
446 r = _crf("--json", *names)
447 assert r.exit_code != 0
448 data = json.loads(r.output)
449 assert data["invalid_count"] == 500
450
451 def test_200_sequential_calls(self) -> None:
452 for _ in range(200):
453 r = _crf("--json", "main")
454 assert r.exit_code == 0
455
456 def test_max_length_boundary(self) -> None:
457 valid = "a" * 255
458 invalid = "a" * 256
459 r = _crf("--json", valid, invalid)
460 data = json.loads(r.output)
461 assert data["valid_count"] == 1
462 assert data["invalid_count"] == 1
463
464 def test_1000_name_stdin_batch(self) -> None:
465 stdin_input = "\n".join(f"feat/task-{i}" for i in range(1000)) + "\n"
466 r = _crf("--json", "--stdin", stdin=stdin_input)
467 assert r.exit_code == 0
468 data = json.loads(r.output)
469 assert data["valid_count"] == 1000
470
471
472 # ---------------------------------------------------------------------------
473 # duration_ms
474 # ---------------------------------------------------------------------------
475
476
477 class TestElapsed:
478 def test_elapsed_in_default_json(self) -> None:
479 r = _crf("--json", "main")
480 data = json.loads(r.output)
481 assert "duration_ms" in data
482 assert isinstance(data["duration_ms"], float)
483 assert data["duration_ms"] >= 0.0
484
485 def test_elapsed_in_rules_json(self) -> None:
486 r = _crf("--json", "--rules")
487 data = json.loads(r.output)
488 assert "duration_ms" in data
489 assert isinstance(data["duration_ms"], float)
490
491 def test_elapsed_absent_from_text_output(self) -> None:
492 r = _crf("main")
493 assert "duration_ms" not in r.output
494
495
496 # ---------------------------------------------------------------------------
497 # exit_code in JSON
498 # ---------------------------------------------------------------------------
499
500
501 class TestExitCode:
502 def test_exit_code_0_when_all_valid(self) -> None:
503 data = json.loads(_crf("--json", "main", "feat/x").output)
504 assert data["exit_code"] == 0
505
506 def test_exit_code_1_when_any_invalid(self) -> None:
507 r = _crf("--json", "good", "bad..name")
508 data = json.loads(r.output)
509 assert data["exit_code"] == 1
510 assert r.exit_code == 1
511
512 def test_exit_code_matches_process_exit(self) -> None:
513 """exit_code in JSON always matches the process exit code."""
514 for names, expected in [
515 (["main"], 0),
516 (["bad..name"], 1),
517 (["main", "bad..name"], 1),
518 ]:
519 r = _crf("--json", *names)
520 data = json.loads(r.output)
521 assert data["exit_code"] == expected
522 assert r.exit_code == expected
523
524 def test_exit_code_in_rules_json(self) -> None:
525 data = json.loads(_crf("--json", "--rules").output)
526 assert "exit_code" in data
527 assert data["exit_code"] == 0
528
529
530 # ---------------------------------------------------------------------------
531 # --invalid-only
532 # ---------------------------------------------------------------------------
533
534
535 class TestInvalidOnly:
536 def test_filters_to_invalid_names(self) -> None:
537 r = _crf("--json", "--invalid-only", "main", "bad..name", "feat/x", ".bad")
538 assert r.exit_code != 0
539 data = json.loads(r.output)
540 assert len(data["results"]) == 2
541 assert all(not res["valid"] for res in data["results"])
542 names = [res["name"] for res in data["results"]]
543 assert "bad..name" in names
544 assert ".bad" in names
545
546 def test_empty_results_when_all_valid(self) -> None:
547 r = _crf("--json", "--invalid-only", "main", "feat/x")
548 assert r.exit_code == 0
549 data = json.loads(r.output)
550 assert data["results"] == []
551 assert data["all_valid"] is True
552
553 def test_all_results_when_all_invalid(self) -> None:
554 r = _crf("--json", "--invalid-only", "bad..a", "bad..b")
555 assert r.exit_code != 0
556 data = json.loads(r.output)
557 assert len(data["results"]) == 2
558
559 def test_counts_reflect_full_set_not_filtered(self) -> None:
560 """valid_count and invalid_count reflect the original full batch."""
561 r = _crf("--json", "--invalid-only", "main", "bad..name", "feat/x")
562 data = json.loads(r.output)
563 assert data["valid_count"] == 2
564 assert data["invalid_count"] == 1
565
566 def test_text_format(self) -> None:
567 r = _crf("--invalid-only", "main", "bad..name")
568 assert r.exit_code != 0
569 assert "main" not in r.output
570 assert "FAIL" in r.output
571 assert "bad..name" in r.output
572
573 def test_stdin_compatible(self) -> None:
574 r = _crf("--json", "--invalid-only", "--stdin", stdin="main\nbad..name\n")
575 data = json.loads(r.output)
576 assert len(data["results"]) == 1
577 assert data["results"][0]["name"] == "bad..name"
578
579 def test_incompatible_with_quiet(self) -> None:
580 r = _crf("--invalid-only", "--quiet", "main")
581 assert r.exit_code != 0
582
583 def test_incompatible_with_rules(self) -> None:
584 r = _crf("--invalid-only", "--rules")
585 assert r.exit_code != 0
586
587
588 # ---------------------------------------------------------------------------
589 # Regression β€” previously incorrect behavior
590 # ---------------------------------------------------------------------------
591
592
593 class TestLeadingDotFix:
594 """Tests for the leading-dot rule that looked correct in tests but
595 previously used monkeypatch.chdir unnecessarily."""
596
597 def test_leading_dot_invalid(self) -> None:
598 r = _crf("--json", ".hidden")
599 assert r.exit_code != 0
600 data = json.loads(r.output)
601 assert data["results"][0]["valid"] is False
602
603 def test_mid_segment_dot_is_valid(self) -> None:
604 """feat/.hidden is valid β€” the leading-dot rule applies to the whole
605 ref name, not each path segment (same behaviour as Git)."""
606 r = _crf("--json", "feat/.hidden")
607 assert r.exit_code == 0
608 assert json.loads(r.output)["all_valid"] is True
609
610
611 # ---------------------------------------------------------------------------
612 # Flag tests
613 # ---------------------------------------------------------------------------
614
615
616 import argparse as _argparse
617
618
619 class TestRegisterFlags:
620 def _parse(self, *args: str) -> _argparse.Namespace:
621 from muse.cli.commands.check_ref_format import register
622 p = _argparse.ArgumentParser()
623 sub = p.add_subparsers()
624 register(sub)
625 return p.parse_args(["check-ref-format", *args])
626
627 def test_default_json_out_is_false(self) -> None:
628 ns = self._parse("main")
629 assert ns.json_out is False
630
631 def test_json_flag_sets_json_out(self) -> None:
632 ns = self._parse("--json", "main")
633 assert ns.json_out is True
634
635 def test_j_shorthand_sets_json_out(self) -> None:
636 ns = self._parse("-j", "main")
637 assert ns.json_out is True