gabriel / muse public
test_cmd_check_ref_format.py python
614 lines 20.8 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 141 days ago
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 _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("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("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("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("x")
136 assert r.exit_code == 0
137
138 def test_255_char_name_valid(self) -> None:
139 name = "a" * 255
140 r = _crf(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("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("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(".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("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("")
211 assert r.exit_code != 0
212
213 def test_256_char_name_too_long(self) -> None:
214 name = "a" * 256
215 r = _crf(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("good", "bad..name")
221 assert r.exit_code != 0
222
223 def test_invalid_count_correct(self) -> None:
224 r = _crf("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("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("..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("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("--format", "text", "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("--format", "text", "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("--format", "text", "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("--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("--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", "--format", "text")
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("--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("--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("--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("--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("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("--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("--format", "text", 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("--format", "text", ansi_name)
407 assert "\x1b" not in r.output
408
409 def test_format_error_goes_to_stderr(self) -> None:
410 r = _crf("--format", "xml", "main")
411 assert r.exit_code != 0
412 assert r.stdout_bytes == b""
413 assert "error" in r.stderr.lower()
414
415 def test_no_traceback_on_bad_format(self) -> None:
416 r = _crf("--format", "bad", "main")
417 assert "Traceback" not in r.output
418 assert "Traceback" not in r.stderr
419
420 def test_null_byte_name_rejected_cleanly(self) -> None:
421 r = _crf("bad\x00name")
422 assert r.exit_code != 0
423 assert "Traceback" not in r.output
424 assert "Traceback" not in r.stderr
425
426 def test_no_args_error_to_stderr(self) -> None:
427 r = _crf()
428 assert r.exit_code != 0
429 assert r.stdout_bytes == b""
430
431 def test_rules_json_no_ansi(self) -> None:
432 r = _crf("--rules")
433 assert "\x1b" not in r.output
434
435
436 # ---------------------------------------------------------------------------
437 # Stress
438 # ---------------------------------------------------------------------------
439
440
441 class TestStress:
442 def test_500_valid_names(self) -> None:
443 names = [f"feat/task-{i:04d}" for i in range(500)]
444 r = _crf(*names)
445 assert r.exit_code == 0
446 data = json.loads(r.output)
447 assert data["valid_count"] == 500
448 assert data["invalid_count"] == 0
449
450 def test_500_invalid_names(self) -> None:
451 names = [f"bad..{i}" for i in range(500)]
452 r = _crf(*names)
453 assert r.exit_code != 0
454 data = json.loads(r.output)
455 assert data["invalid_count"] == 500
456
457 def test_200_sequential_calls(self) -> None:
458 for _ in range(200):
459 r = _crf("main")
460 assert r.exit_code == 0
461
462 def test_max_length_boundary(self) -> None:
463 valid = "a" * 255
464 invalid = "a" * 256
465 r = _crf(valid, invalid)
466 data = json.loads(r.output)
467 assert data["valid_count"] == 1
468 assert data["invalid_count"] == 1
469
470 def test_1000_name_stdin_batch(self) -> None:
471 stdin_input = "\n".join(f"feat/task-{i}" for i in range(1000)) + "\n"
472 r = _crf("--stdin", stdin=stdin_input)
473 assert r.exit_code == 0
474 data = json.loads(r.output)
475 assert data["valid_count"] == 1000
476
477
478 # ---------------------------------------------------------------------------
479 # duration_ms
480 # ---------------------------------------------------------------------------
481
482
483 class TestElapsed:
484 def test_elapsed_in_default_json(self) -> None:
485 r = _crf("main")
486 data = json.loads(r.output)
487 assert "duration_ms" in data
488 assert isinstance(data["duration_ms"], float)
489 assert data["duration_ms"] >= 0.0
490
491 def test_elapsed_in_rules_json(self) -> None:
492 r = _crf("--rules")
493 data = json.loads(r.output)
494 assert "duration_ms" in data
495 assert isinstance(data["duration_ms"], float)
496
497 def test_elapsed_absent_from_text_output(self) -> None:
498 r = _crf("--format", "text", "main")
499 assert "duration_ms" not in r.output
500
501
502 # ---------------------------------------------------------------------------
503 # exit_code in JSON
504 # ---------------------------------------------------------------------------
505
506
507 class TestExitCode:
508 def test_exit_code_0_when_all_valid(self) -> None:
509 data = json.loads(_crf("main", "feat/x").output)
510 assert data["exit_code"] == 0
511
512 def test_exit_code_1_when_any_invalid(self) -> None:
513 r = _crf("good", "bad..name")
514 data = json.loads(r.output)
515 assert data["exit_code"] == 1
516 assert r.exit_code == 1
517
518 def test_exit_code_matches_process_exit(self) -> None:
519 """exit_code in JSON always matches the process exit code."""
520 for names, expected in [
521 (["main"], 0),
522 (["bad..name"], 1),
523 (["main", "bad..name"], 1),
524 ]:
525 r = _crf(*names)
526 data = json.loads(r.output)
527 assert data["exit_code"] == expected
528 assert r.exit_code == expected
529
530 def test_exit_code_in_rules_json(self) -> None:
531 data = json.loads(_crf("--rules").output)
532 assert "exit_code" in data
533 assert data["exit_code"] == 0
534
535
536 # ---------------------------------------------------------------------------
537 # --invalid-only
538 # ---------------------------------------------------------------------------
539
540
541 class TestInvalidOnly:
542 def test_filters_to_invalid_names(self) -> None:
543 r = _crf("--invalid-only", "main", "bad..name", "feat/x", ".bad")
544 assert r.exit_code != 0
545 data = json.loads(r.output)
546 assert len(data["results"]) == 2
547 assert all(not res["valid"] for res in data["results"])
548 names = [res["name"] for res in data["results"]]
549 assert "bad..name" in names
550 assert ".bad" in names
551
552 def test_empty_results_when_all_valid(self) -> None:
553 r = _crf("--invalid-only", "main", "feat/x")
554 assert r.exit_code == 0
555 data = json.loads(r.output)
556 assert data["results"] == []
557 assert data["all_valid"] is True
558
559 def test_all_results_when_all_invalid(self) -> None:
560 r = _crf("--invalid-only", "bad..a", "bad..b")
561 assert r.exit_code != 0
562 data = json.loads(r.output)
563 assert len(data["results"]) == 2
564
565 def test_counts_reflect_full_set_not_filtered(self) -> None:
566 """valid_count and invalid_count reflect the original full batch."""
567 r = _crf("--invalid-only", "main", "bad..name", "feat/x")
568 data = json.loads(r.output)
569 assert data["valid_count"] == 2
570 assert data["invalid_count"] == 1
571
572 def test_text_format(self) -> None:
573 r = _crf("--invalid-only", "--format", "text", "main", "bad..name")
574 assert r.exit_code != 0
575 assert "main" not in r.output
576 assert "FAIL" in r.output
577 assert "bad..name" in r.output
578
579 def test_stdin_compatible(self) -> None:
580 r = _crf("--invalid-only", "--stdin", stdin="main\nbad..name\n")
581 data = json.loads(r.output)
582 assert len(data["results"]) == 1
583 assert data["results"][0]["name"] == "bad..name"
584
585 def test_incompatible_with_quiet(self) -> None:
586 r = _crf("--invalid-only", "--quiet", "main")
587 assert r.exit_code != 0
588
589 def test_incompatible_with_rules(self) -> None:
590 r = _crf("--invalid-only", "--rules")
591 assert r.exit_code != 0
592
593
594 # ---------------------------------------------------------------------------
595 # Regression — previously incorrect behavior
596 # ---------------------------------------------------------------------------
597
598
599 class TestLeadingDotFix:
600 """Tests for the leading-dot rule that looked correct in tests but
601 previously used monkeypatch.chdir unnecessarily."""
602
603 def test_leading_dot_invalid(self) -> None:
604 r = _crf(".hidden")
605 assert r.exit_code != 0
606 data = json.loads(r.output)
607 assert data["results"][0]["valid"] is False
608
609 def test_mid_segment_dot_is_valid(self) -> None:
610 """feat/.hidden is valid — the leading-dot rule applies to the whole
611 ref name, not each path segment (same behaviour as Git)."""
612 r = _crf("feat/.hidden")
613 assert r.exit_code == 0
614 assert json.loads(r.output)["all_valid"] is True
File History 2 commits
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 141 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 144 days ago