gabriel / muse public
test_plumbing_check_ref_format.py python
492 lines 16.5 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
1 """Tests for muse plumbing 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.plumbing.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_rules_max_length(self) -> None:
79 assert _RULES["max_length"] == 255
80
81 def test_rules_forbidden_chars_includes_c0_controls(self) -> None:
82 """Null byte is covered by the C0 controls group."""
83 forbidden_chars = _RULES["forbidden_chars"]
84 # Either the literal null byte or a descriptive C0 group string is acceptable.
85 has_null = "\x00" in forbidden_chars
86 has_c0_group = any("C0" in s or "0x00" in s for s in forbidden_chars)
87 assert has_null or has_c0_group, "null byte must be covered in forbidden_chars"
88
89 def test_rules_forbidden_chars_includes_backslash(self) -> None:
90 assert "\\" in _RULES["forbidden_chars"]
91
92 def test_rules_forbidden_patterns_not_empty(self) -> None:
93 assert len(_RULES["forbidden_patterns"]) >= 4
94
95 def test_rules_notes_mentions_slash_ok(self) -> None:
96 assert "/" in _RULES["notes"] or "slash" in _RULES["notes"].lower()
97
98
99 # ---------------------------------------------------------------------------
100 # Integration — valid names
101 # ---------------------------------------------------------------------------
102
103
104 class TestValidNames:
105 def test_simple_name(self) -> None:
106 r = _crf("main")
107 assert r.exit_code == 0
108 data = json.loads(r.output)
109 assert data["all_valid"] is True
110 assert data["results"][0]["valid"] is True
111 assert data["results"][0]["error"] is None
112
113 def test_namespaced_name(self) -> None:
114 r = _crf("feat/my-branch")
115 assert r.exit_code == 0
116 data = json.loads(r.output)
117 assert data["all_valid"] is True
118
119 def test_deeply_hierarchical_name(self) -> None:
120 r = _crf("team/feat/PROJ-42/wip")
121 assert r.exit_code == 0
122 assert json.loads(r.output)["all_valid"] is True
123
124 def test_name_with_numbers(self) -> None:
125 r = _crf("release-2026-03-27")
126 assert r.exit_code == 0
127
128 def test_single_char_name(self) -> None:
129 r = _crf("x")
130 assert r.exit_code == 0
131
132 def test_255_char_name_valid(self) -> None:
133 name = "a" * 255
134 r = _crf(name)
135 assert r.exit_code == 0
136 assert json.loads(r.output)["all_valid"] is True
137
138 def test_all_valid_exits_zero(self) -> None:
139 r = _crf("feat/a", "fix/b", "dev", "main")
140 assert r.exit_code == 0
141 data = json.loads(r.output)
142 assert data["all_valid"] is True
143 assert data["valid_count"] == 4
144 assert data["invalid_count"] == 0
145
146
147 # ---------------------------------------------------------------------------
148 # Integration — invalid names (each rule)
149 # ---------------------------------------------------------------------------
150
151
152 class TestInvalidNames:
153 def test_consecutive_dots(self) -> None:
154 r = _crf("bad..name")
155 assert r.exit_code != 0
156 data = json.loads(r.output)
157 assert data["all_valid"] is False
158 assert data["results"][0]["valid"] is False
159 assert data["results"][0]["error"] is not None
160
161 def test_leading_dot(self) -> None:
162 r = _crf(".hidden")
163 assert r.exit_code != 0
164 assert json.loads(r.output)["all_valid"] is False
165
166 def test_trailing_dot(self) -> None:
167 r = _crf("trailing.")
168 assert r.exit_code != 0
169 assert json.loads(r.output)["all_valid"] is False
170
171 def test_leading_slash(self) -> None:
172 r = _crf("/bad")
173 assert r.exit_code != 0
174
175 def test_trailing_slash(self) -> None:
176 r = _crf("bad/")
177 assert r.exit_code != 0
178
179 def test_consecutive_slashes(self) -> None:
180 r = _crf("bad//name")
181 assert r.exit_code != 0
182
183 def test_null_byte(self) -> None:
184 r = _crf("bad\x00name")
185 assert r.exit_code != 0
186
187 def test_backslash(self) -> None:
188 r = _crf("bad\\name")
189 assert r.exit_code != 0
190
191 def test_tab_character(self) -> None:
192 r = _crf("bad\tname")
193 assert r.exit_code != 0
194
195 def test_carriage_return(self) -> None:
196 r = _crf("bad\rname")
197 assert r.exit_code != 0
198
199 def test_newline(self) -> None:
200 r = _crf("bad\nname")
201 assert r.exit_code != 0
202
203 def test_empty_name(self) -> None:
204 r = _crf("")
205 assert r.exit_code != 0
206
207 def test_256_char_name_too_long(self) -> None:
208 name = "a" * 256
209 r = _crf(name)
210 assert r.exit_code != 0
211 assert json.loads(r.output)["all_valid"] is False
212
213 def test_any_invalid_exits_nonzero(self) -> None:
214 r = _crf("good", "bad..name")
215 assert r.exit_code != 0
216
217 def test_invalid_count_correct(self) -> None:
218 r = _crf("good", "bad..name", ".also-bad")
219 data = json.loads(r.output)
220 assert data["valid_count"] == 1
221 assert data["invalid_count"] == 2
222
223
224 # ---------------------------------------------------------------------------
225 # Integration — valid_count / invalid_count fields
226 # ---------------------------------------------------------------------------
227
228
229 class TestCountFields:
230 def test_all_valid_counts(self) -> None:
231 r = _crf("a", "b", "c")
232 data = json.loads(r.output)
233 assert data["valid_count"] == 3
234 assert data["invalid_count"] == 0
235
236 def test_all_invalid_counts(self) -> None:
237 r = _crf("..a", "..b")
238 data = json.loads(r.output)
239 assert data["valid_count"] == 0
240 assert data["invalid_count"] == 2
241
242 def test_mixed_counts(self) -> None:
243 r = _crf("good", "..bad", "also-good", ".also-bad")
244 data = json.loads(r.output)
245 assert data["valid_count"] == 2
246 assert data["invalid_count"] == 2
247
248
249 # ---------------------------------------------------------------------------
250 # Integration — --quiet mode
251 # ---------------------------------------------------------------------------
252
253
254 class TestQuietMode:
255 def test_quiet_valid_exits_zero_no_output(self) -> None:
256 r = _crf("--quiet", "main")
257 assert r.exit_code == 0
258 assert r.output.strip() == ""
259
260 def test_quiet_invalid_exits_nonzero_no_output(self) -> None:
261 r = _crf("-q", "bad..name")
262 assert r.exit_code != 0
263 assert r.output.strip() == ""
264
265 def test_quiet_mixed_exits_nonzero(self) -> None:
266 r = _crf("--quiet", "good", "bad..name")
267 assert r.exit_code != 0
268
269
270 # ---------------------------------------------------------------------------
271 # Integration — text output
272 # ---------------------------------------------------------------------------
273
274
275 class TestTextOutput:
276 def test_valid_shows_ok(self) -> None:
277 r = _crf("--format", "text", "main")
278 assert r.exit_code == 0
279 assert "ok" in r.output
280
281 def test_invalid_shows_fail(self) -> None:
282 r = _crf("--format", "text", "bad..name")
283 assert r.exit_code != 0
284 assert "FAIL" in r.output
285
286 def test_mixed_shows_both(self) -> None:
287 r = _crf("--format", "text", "good", "bad..name")
288 assert r.exit_code != 0
289 assert "ok" in r.output
290 assert "FAIL" in r.output
291
292 def test_json_shorthand_alias(self) -> None:
293 r = _crf("--json", "main")
294 assert r.exit_code == 0
295 data = json.loads(r.output)
296 assert "results" in data
297
298
299 # ---------------------------------------------------------------------------
300 # Integration — --rules
301 # ---------------------------------------------------------------------------
302
303
304 class TestRules:
305 def test_rules_json_output(self) -> None:
306 r = _crf("--rules")
307 assert r.exit_code == 0
308 data = json.loads(r.output)
309 assert "max_length" in data
310 assert "forbidden_chars" in data
311 assert "forbidden_patterns" in data
312 assert "notes" in data
313
314 def test_rules_max_length_is_255(self) -> None:
315 r = _crf("--rules")
316 data = json.loads(r.output)
317 assert data["max_length"] == 255
318
319 def test_rules_text_format(self) -> None:
320 r = _crf("--rules", "--format", "text")
321 assert r.exit_code == 0
322 assert "max_length" in r.output
323 assert "forbidden" in r.output.lower()
324
325 def test_rules_needs_no_names(self) -> None:
326 """--rules exits cleanly with no name arguments."""
327 r = _crf("--rules")
328 assert r.exit_code == 0
329
330 def test_rules_json_is_parseable(self) -> None:
331 r = _crf("--rules", "--json")
332 assert r.exit_code == 0
333 data = json.loads(r.output)
334 assert isinstance(data["forbidden_chars"], list)
335 assert isinstance(data["forbidden_patterns"], list)
336
337
338 # ---------------------------------------------------------------------------
339 # Integration — --stdin
340 # ---------------------------------------------------------------------------
341
342
343 class TestStdinMode:
344 def test_stdin_reads_names(self) -> None:
345 r = _crf("--stdin", stdin="main\ndev\n")
346 assert r.exit_code == 0
347 data = json.loads(r.output)
348 assert data["valid_count"] == 2
349
350 def test_stdin_skips_blank_lines(self) -> None:
351 r = _crf("--stdin", stdin="\nmain\n\ndev\n\n")
352 assert r.exit_code == 0
353 data = json.loads(r.output)
354 assert len(data["results"]) == 2
355
356 def test_stdin_skips_comments(self) -> None:
357 r = _crf("--stdin", stdin="# a comment\nmain\n")
358 assert r.exit_code == 0
359 data = json.loads(r.output)
360 assert len(data["results"]) == 1
361
362 def test_stdin_combined_with_positional(self) -> None:
363 r = _crf("main", "--stdin", stdin="dev\n")
364 assert r.exit_code == 0
365 data = json.loads(r.output)
366 assert len(data["results"]) == 2
367
368 def test_stdin_invalid_name_from_stdin(self) -> None:
369 r = _crf("--stdin", stdin="bad..name\n")
370 assert r.exit_code != 0
371 data = json.loads(r.output)
372 assert data["all_valid"] is False
373
374 def test_stdin_empty_with_no_positional_errors(self) -> None:
375 r = _crf("--stdin", stdin="")
376 assert r.exit_code != 0
377 assert r.stdout_bytes == b""
378
379 def test_stdin_only_comments_errors(self) -> None:
380 r = _crf("--stdin", stdin="# comment only\n")
381 assert r.exit_code != 0
382 assert r.stdout_bytes == b""
383
384
385 # ---------------------------------------------------------------------------
386 # Security
387 # ---------------------------------------------------------------------------
388
389
390 class TestSecurity:
391 def test_ansi_in_name_stripped_text_output(self) -> None:
392 """ANSI escape in a branch name must not appear raw in text output."""
393 ansi_name = "\x1b[31mbadname\x1b[0m"
394 r = _crf("--format", "text", ansi_name)
395 assert "\x1b" not in r.output
396
397 def test_ansi_in_error_message_stripped(self) -> None:
398 """If the error message echoes the name, it must be sanitized."""
399 ansi_name = "\x1b[31m.leading\x1b[0m"
400 r = _crf("--format", "text", ansi_name)
401 assert "\x1b" not in r.output
402
403 def test_format_error_goes_to_stderr(self) -> None:
404 r = _crf("--format", "xml", "main")
405 assert r.exit_code != 0
406 assert r.stdout_bytes == b""
407 assert "error" in r.stderr.lower()
408
409 def test_no_traceback_on_bad_format(self) -> None:
410 r = _crf("--format", "bad", "main")
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(*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(*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("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(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("--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 # Regression — previously incorrect behavior
474 # ---------------------------------------------------------------------------
475
476
477 class TestLeadingDotFix:
478 """Tests for the leading-dot rule that looked correct in tests but
479 previously used monkeypatch.chdir unnecessarily."""
480
481 def test_leading_dot_invalid(self) -> None:
482 r = _crf(".hidden")
483 assert r.exit_code != 0
484 data = json.loads(r.output)
485 assert data["results"][0]["valid"] is False
486
487 def test_mid_segment_dot_is_valid(self) -> None:
488 """feat/.hidden is valid — the leading-dot rule applies to the whole
489 ref name, not each path segment (same behaviour as Git)."""
490 r = _crf("feat/.hidden")
491 assert r.exit_code == 0
492 assert json.loads(r.output)["all_valid"] is True
File History 3 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 100 days ago