gabriel / muse public

test_cmd_describe_hardening.py file-level

at sha256:2 · 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 """Hardening test suite for ``muse describe``.
2
3 Coverage:
4 - Unit: describe_commit core — no tags, exact, distance, long, match_pattern,
5 first_parent, abbrev, exact_match, _MAX_WALK budget, multi-tag tie-break
6 - Security: ANSI injection in tag names sanitized in text output,
7 raw in JSON; --ref ANSI passthrough in error message
8 - Error routing: all user errors routed to stderr
9 - JSON schema: _DescribeJson shape, all fields present, repo_id + branch
10 - New flags: --match, --exact-match, --first-parent, --abbrev, --json
11 - Integration: tag walk across merge commits, --first-parent vs full walk
12 - E2E: help output, combined flags
13 - Stress: 5 000-commit chain, 200-tag repo, concurrent reads
14 """
15
16 from __future__ import annotations
17
18 import datetime
19 import json
20 import pathlib
21 import threading
22 from typing import TypedDict
23 from unittest.mock import patch
24
25 import pytest
26 from tests.cli_test_helper import CliRunner, InvokeResult
27
28 from muse.core.describe import describe_commit, _MAX_WALK
29 from muse.core.object_store import write_object
30 from muse.core.ids import hash_commit, hash_snapshot
31 from muse.core.commits import (
32 CommitRecord,
33 write_commit,
34 )
35 from muse.core.snapshots import (
36 SnapshotRecord,
37 write_snapshot,
38 )
39 from muse.core.tags import (
40 TagRecord,
41 write_tag,
42 )
43 from muse.core.types import Manifest, blob_id, fake_id, content_hash
44 from muse.core.paths import muse_dir, ref_path
45
46 runner = CliRunner()
47 _REPO_ID = content_hash({"name": "describe-hard-test"})
48
49
50 # ---------------------------------------------------------------------------
51 # Helpers
52 # ---------------------------------------------------------------------------
53
54
55
56
57 def _init_repo(path: pathlib.Path, *, domain: str = "midi") -> pathlib.Path:
58 dot_muse = muse_dir(path)
59 for sub in ("commits", "snapshots", "objects", "refs/heads", "tags"):
60 (dot_muse / sub).mkdir(parents=True, exist_ok=True)
61 (dot_muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
62 (dot_muse / "repo.json").write_text(
63 json.dumps({"repo_id": _REPO_ID, "domain": domain}),
64 encoding="utf-8",
65 )
66 return path
67
68
69 def _make_commit(
70 root: pathlib.Path,
71 parent_id: str | None = None,
72 parent2_id: str | None = None,
73 content: bytes = b"data",
74 branch: str = "main",
75 ) -> str:
76 obj_id = blob_id(content)
77 write_object(root, obj_id, content)
78 manifest = {f"f_{obj_id[len('sha256:'):len('sha256:') + 8]}.txt": obj_id}
79 snap_id = hash_snapshot(manifest)
80 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
81 committed_at = datetime.datetime.now(datetime.timezone.utc)
82 parent_ids = [pid for pid in (parent_id, parent2_id) if pid is not None]
83 commit_id = hash_commit(
84 parent_ids=parent_ids,
85 snapshot_id=snap_id,
86 message="msg",
87 committed_at_iso=committed_at.isoformat(),
88 )
89 rec = CommitRecord(
90 commit_id=commit_id,
91 branch=branch,
92 snapshot_id=snap_id,
93 message="msg",
94 committed_at=committed_at,
95 parent_commit_id=parent_id,
96 parent2_commit_id=parent2_id,
97 )
98 write_commit(root, rec)
99 (ref_path(root, branch)).write_text(
100 commit_id, encoding="utf-8"
101 )
102 return commit_id
103
104
105 def _make_tag(root: pathlib.Path, tag: str, commit_id: str) -> None:
106 write_tag(
107 root,
108 TagRecord(
109 tag_id=content_hash({"tag": tag, "commit_id": commit_id}),
110 repo_id=_REPO_ID,
111 tag=tag,
112 commit_id=commit_id,
113 created_at=datetime.datetime.now(datetime.timezone.utc),
114 ),
115 )
116
117
118 def _env(repo: pathlib.Path) -> Manifest:
119 return {"MUSE_REPO_ROOT": str(repo)}
120
121
122 def _invoke(args: list[str], env: Manifest) -> InvokeResult:
123 return runner.invoke(None, args, env=env)
124
125
126 class _DescribeOut(TypedDict):
127 commit_id: str
128 tag: str | None
129 distance: int
130 short_sha: str
131 name: str
132 exact: bool
133 repo_id: str
134 branch: str
135 duration_ms: float
136 exit_code: int
137
138
139 def _parse_json(result: InvokeResult) -> _DescribeOut:
140 raw = json.loads(result.output.strip())
141 return _DescribeOut(
142 commit_id=raw["commit_id"],
143 tag=raw["tag"],
144 distance=raw["distance"],
145 short_sha=raw["short_sha"],
146 name=raw["name"],
147 exact=raw["exact"],
148 repo_id=raw["repo_id"],
149 branch=raw["branch"],
150 duration_ms=raw["duration_ms"],
151 exit_code=raw["exit_code"],
152 )
153
154
155 # ---------------------------------------------------------------------------
156 # Unit: describe_commit core
157 # ---------------------------------------------------------------------------
158
159
160 def test_core_no_tags_returns_shortblob_id(tmp_path: pathlib.Path) -> None:
161 _init_repo(tmp_path)
162 cid = _make_commit(tmp_path, content=b"a")
163 r = describe_commit(tmp_path, _REPO_ID, cid)
164 assert r["tag"] is None
165 assert r["short_sha"] == cid[:len("sha256:") + 12]
166 assert r["name"] == r["short_sha"]
167 assert r["exact"] is False
168
169
170 def test_core_exact_tag(tmp_path: pathlib.Path) -> None:
171 _init_repo(tmp_path)
172 cid = _make_commit(tmp_path, content=b"b")
173 _make_tag(tmp_path, "v1.0.0", cid)
174 r = describe_commit(tmp_path, _REPO_ID, cid)
175 assert r["tag"] == "v1.0.0"
176 assert r["distance"] == 0
177 assert r["exact"] is True
178 assert r["name"] == "v1.0.0"
179
180
181 def test_core_distance_one(tmp_path: pathlib.Path) -> None:
182 _init_repo(tmp_path)
183 c1 = _make_commit(tmp_path, content=b"c1")
184 _make_tag(tmp_path, "v0.9", c1)
185 c2 = _make_commit(tmp_path, parent_id=c1, content=b"c2")
186 r = describe_commit(tmp_path, _REPO_ID, c2)
187 assert r["tag"] == "v0.9"
188 assert r["distance"] == 1
189 assert r["exact"] is False
190 assert r["name"] == "v0.9~1"
191
192
193 def test_core_long_format_on_tag(tmp_path: pathlib.Path) -> None:
194 _init_repo(tmp_path)
195 cid = _make_commit(tmp_path, content=b"long")
196 _make_tag(tmp_path, "v2.0.0", cid)
197 r = describe_commit(tmp_path, _REPO_ID, cid, long_format=True)
198 assert r["name"].startswith("v2.0.0-0-sha256:")
199
200
201 def test_core_long_format_with_distance(tmp_path: pathlib.Path) -> None:
202 _init_repo(tmp_path)
203 c1 = _make_commit(tmp_path, content=b"root")
204 _make_tag(tmp_path, "v1.0", c1)
205 c2 = _make_commit(tmp_path, parent_id=c1, content=b"next")
206 r = describe_commit(tmp_path, _REPO_ID, c2, long_format=True)
207 assert "-1-sha256:" in r["name"]
208
209
210 def test_core_abbrev_controls_sha_length(tmp_path: pathlib.Path) -> None:
211 _init_repo(tmp_path)
212 cid = _make_commit(tmp_path, content=b"abbrev")
213 r = describe_commit(tmp_path, _REPO_ID, cid, abbrev=8)
214 assert r["short_sha"] == cid[:len("sha256:") + 8]
215 assert r["short_sha"].startswith("sha256:")
216 assert len(r["short_sha"]) == len("sha256:") + 8
217
218
219 def test_core_match_pattern_filters_tags(tmp_path: pathlib.Path) -> None:
220 _init_repo(tmp_path)
221 cid = _make_commit(tmp_path, content=b"match")
222 _make_tag(tmp_path, "release-1", cid)
223 _make_tag(tmp_path, "v1.0.0", cid)
224 # Only semver tags — "release-1" excluded.
225 r = describe_commit(tmp_path, _REPO_ID, cid, match_pattern="v*")
226 assert r["tag"] == "v1.0.0"
227
228
229 def test_core_match_pattern_no_match_returnsblob_id(tmp_path: pathlib.Path) -> None:
230 _init_repo(tmp_path)
231 cid = _make_commit(tmp_path, content=b"nomatch")
232 _make_tag(tmp_path, "nightly-123", cid)
233 r = describe_commit(tmp_path, _REPO_ID, cid, match_pattern="v*")
234 assert r["tag"] is None
235 assert r["name"] == cid[:len("sha256:") + 12]
236
237
238 def test_core_exact_match_on_tag(tmp_path: pathlib.Path) -> None:
239 _init_repo(tmp_path)
240 cid = _make_commit(tmp_path, content=b"exact")
241 _make_tag(tmp_path, "v3.0", cid)
242 r = describe_commit(tmp_path, _REPO_ID, cid, exact_match=True)
243 assert r["tag"] == "v3.0"
244 assert r["exact"] is True
245
246
247 def test_core_exact_match_off_tag_returnsblob_id(tmp_path: pathlib.Path) -> None:
248 _init_repo(tmp_path)
249 c1 = _make_commit(tmp_path, content=b"root")
250 _make_tag(tmp_path, "v3.0", c1)
251 c2 = _make_commit(tmp_path, parent_id=c1, content=b"after")
252 r = describe_commit(tmp_path, _REPO_ID, c2, exact_match=True)
253 assert r["tag"] is None
254 assert r["name"] == c2[:len("sha256:") + 12]
255
256
257 def test_core_first_parent_skips_merge_branch(tmp_path: pathlib.Path) -> None:
258 """With --first-parent the tag on a merged branch is invisible."""
259 _init_repo(tmp_path)
260 # main: c1 → c3 (merge of feat)
261 # feat: c1 → c2 (tag here)
262 c1 = _make_commit(tmp_path, content=b"root")
263 c2 = _make_commit(tmp_path, parent_id=c1, content=b"feat", branch="feat")
264 _make_tag(tmp_path, "feat-tag", c2)
265 c3 = _make_commit(
266 tmp_path, parent_id=c1, parent2_id=c2, content=b"merge", branch="main"
267 )
268 # Without first_parent: feat-tag is reachable via second parent.
269 r_full = describe_commit(tmp_path, _REPO_ID, c3)
270 # With first_parent: only first parent chain; feat-tag not reachable.
271 r_fp = describe_commit(tmp_path, _REPO_ID, c3, first_parent=True)
272 assert r_fp["tag"] is None
273 # Full walk should find the tag (via c2).
274 assert r_full["tag"] == "feat-tag"
275
276
277 def test_core_multi_tag_same_commit_lex_greatest(tmp_path: pathlib.Path) -> None:
278 """When multiple tags point at the same commit, greatest lex name wins."""
279 _init_repo(tmp_path)
280 cid = _make_commit(tmp_path, content=b"multi")
281 _make_tag(tmp_path, "v1.0.0", cid)
282 _make_tag(tmp_path, "v2.0.0", cid)
283 _make_tag(tmp_path, "v1.5.0", cid)
284 r = describe_commit(tmp_path, _REPO_ID, cid)
285 assert r["tag"] == "v2.0.0"
286
287
288 def test_core_max_walk_budget(tmp_path: pathlib.Path) -> None:
289 """Walk stops at _MAX_WALK without crashing; returns short-SHA fallback."""
290 _init_repo(tmp_path)
291 # Inject a fake read_commit that always returns a parent so BFS never
292 # finds a commit-store miss — we just want to trigger the budget guard.
293 cid = _make_commit(tmp_path, content=b"budget")
294 _make_tag(tmp_path, "very-far", cid)
295
296 call_count = 0
297
298 import muse.core.describe as _describe_mod
299 from muse.core.commits import read_commit as _orig_read_commit, CommitRecord as _CR
300 import datetime as _dt
301
302 orig = _orig_read_commit
303
304 def _fake_read(root: pathlib.Path, cid: str) -> _CR | None:
305 nonlocal call_count
306 call_count += 1
307 if call_count > _MAX_WALK + 5:
308 return None
309 fake_parent = fake_id(cid)
310 return _CR(
311 commit_id=cid,
312 branch="main",
313 snapshot_id="snap",
314 message="x",
315 committed_at=_dt.datetime.now(_dt.timezone.utc),
316 parent_commit_id=fake_parent,
317 )
318
319 with patch.object(_describe_mod, "read_commit", side_effect=_fake_read):
320 # Start from a commit far from any tag — BFS will exhaust budget.
321 far_commit = blob_id(b"far")
322 r = describe_commit(tmp_path, _REPO_ID, far_commit)
323
324 # Budget exhausted → tag not found → name is short SHA.
325 assert r["tag"] is None
326
327
328 # ---------------------------------------------------------------------------
329 # Security: ANSI injection in tag names
330 # ---------------------------------------------------------------------------
331
332
333 def test_ansi_in_tag_name_stripped_in_text_output(tmp_path: pathlib.Path) -> None:
334 _init_repo(tmp_path)
335 cid = _make_commit(tmp_path, content=b"ansi")
336 malicious_tag = "v1.0\x1b[31mRED\x1b[0m"
337 _make_tag(tmp_path, malicious_tag, cid)
338 result = _invoke(["describe"], _env(tmp_path))
339 assert result.exit_code == 0
340 assert "\x1b[31m" not in result.output
341
342
343 def test_ansi_in_tag_name_preserved_in_json(tmp_path: pathlib.Path) -> None:
344 """JSON output must not sanitize so callers see the raw value."""
345 _init_repo(tmp_path)
346 cid = _make_commit(tmp_path, content=b"ansi-json")
347 malicious_tag = "v1.0\x1b[31mRED\x1b[0m"
348 _make_tag(tmp_path, malicious_tag, cid)
349 result = _invoke(["describe", "--json"], _env(tmp_path))
350 assert result.exit_code == 0
351 data = _parse_json(result)
352 assert data["tag"] == malicious_tag
353
354
355 # ---------------------------------------------------------------------------
356 # Error routing: all user errors go to stderr
357 # ---------------------------------------------------------------------------
358
359
360 def test_no_commits_error_on_stderr(tmp_path: pathlib.Path) -> None:
361 _init_repo(tmp_path)
362 result = _invoke(["describe"], _env(tmp_path))
363 assert result.exit_code != 0
364 assert result.stderr != "" or "commits" in result.output.lower()
365
366
367 def test_ref_not_found_error_on_stderr(tmp_path: pathlib.Path) -> None:
368 _init_repo(tmp_path)
369 _make_commit(tmp_path, content=b"x")
370 result = _invoke(["describe", "--ref", "nonexistent"], _env(tmp_path))
371 assert result.exit_code != 0
372
373
374 def test_require_tag_no_tags_error(tmp_path: pathlib.Path) -> None:
375 _init_repo(tmp_path)
376 _make_commit(tmp_path, content=b"no-tag")
377 result = _invoke(["describe", "--require-tag"], _env(tmp_path))
378 assert result.exit_code != 0
379
380
381 def test_exact_match_not_on_tag_error(tmp_path: pathlib.Path) -> None:
382 _init_repo(tmp_path)
383 c1 = _make_commit(tmp_path, content=b"c1")
384 _make_tag(tmp_path, "v1", c1)
385 _make_commit(tmp_path, parent_id=c1, content=b"c2")
386 result = _invoke(["describe", "--exact-match"], _env(tmp_path))
387 assert result.exit_code != 0
388
389
390 def test_abbrev_too_small_error(tmp_path: pathlib.Path) -> None:
391 _init_repo(tmp_path)
392 _make_commit(tmp_path, content=b"ab")
393 result = _invoke(["describe", "--abbrev", "2"], _env(tmp_path))
394 assert result.exit_code != 0
395
396
397 def test_abbrev_too_large_error(tmp_path: pathlib.Path) -> None:
398 _init_repo(tmp_path)
399 _make_commit(tmp_path, content=b"ab")
400 result = _invoke(["describe", "--abbrev", "65"], _env(tmp_path))
401 assert result.exit_code != 0
402
403
404 # ---------------------------------------------------------------------------
405 # JSON schema: _DescribeJson
406 # ---------------------------------------------------------------------------
407
408
409 def test_json_schema_all_fields(tmp_path: pathlib.Path) -> None:
410 _init_repo(tmp_path)
411 cid = _make_commit(tmp_path, content=b"schema")
412 _make_tag(tmp_path, "v1.0.0", cid)
413 result = _invoke(["describe", "--json"], _env(tmp_path))
414 assert result.exit_code == 0
415 data = _parse_json(result)
416 assert data["tag"] == "v1.0.0"
417 assert data["distance"] == 0
418 assert data["exact"] is True
419 assert data["repo_id"] == _REPO_ID
420 assert data["branch"] == "main"
421 assert data["commit_id"] == cid
422 assert data["short_sha"] == cid[:len("sha256:") + 12]
423
424
425 def test_json_schema_no_tag(tmp_path: pathlib.Path) -> None:
426 _init_repo(tmp_path)
427 cid = _make_commit(tmp_path, content=b"no-tag-json")
428 result = _invoke(["describe", "--json"], _env(tmp_path))
429 assert result.exit_code == 0
430 data = _parse_json(result)
431 assert data["tag"] is None
432 assert data["name"] == cid[:len("sha256:") + 12]
433 assert data["exact"] is False
434
435
436 def test_json_schema_with_distance(tmp_path: pathlib.Path) -> None:
437 _init_repo(tmp_path)
438 c1 = _make_commit(tmp_path, content=b"root")
439 _make_tag(tmp_path, "v0.1", c1)
440 _make_commit(tmp_path, parent_id=c1, content=b"next")
441 result = _invoke(["describe", "--json"], _env(tmp_path))
442 assert result.exit_code == 0
443 data = _parse_json(result)
444 assert data["tag"] == "v0.1"
445 assert data["distance"] == 1
446 assert data["exact"] is False
447
448
449 def test_json_abbrev_reflected(tmp_path: pathlib.Path) -> None:
450 _init_repo(tmp_path)
451 cid = _make_commit(tmp_path, content=b"abbrev-json")
452 result = _invoke(["describe", "--abbrev", "8", "--json"], _env(tmp_path))
453 assert result.exit_code == 0
454 data = _parse_json(result)
455 assert data["short_sha"].startswith("sha256:")
456 assert len(data["short_sha"]) == len("sha256:") + 8
457
458
459 # ---------------------------------------------------------------------------
460 # New flags: --match, --exact-match, --first-parent, --abbrev
461 # ---------------------------------------------------------------------------
462
463
464 def test_flag_match_filters_tags(tmp_path: pathlib.Path) -> None:
465 _init_repo(tmp_path)
466 cid = _make_commit(tmp_path, content=b"match-flag")
467 _make_tag(tmp_path, "nightly-1", cid)
468 _make_tag(tmp_path, "v1.0.0", cid)
469 result = _invoke(["describe", "--match", "v*", "--json"], _env(tmp_path))
470 assert result.exit_code == 0
471 data = _parse_json(result)
472 assert data["tag"] == "v1.0.0"
473
474
475 def test_flag_match_no_matching_tag(tmp_path: pathlib.Path) -> None:
476 _init_repo(tmp_path)
477 cid = _make_commit(tmp_path, content=b"match-none")
478 _make_tag(tmp_path, "nightly-1", cid)
479 result = _invoke(["describe", "--match", "v*", "--json"], _env(tmp_path))
480 assert result.exit_code == 0
481 data = _parse_json(result)
482 assert data["tag"] is None
483
484
485 def test_flag_exact_match_on_tag(tmp_path: pathlib.Path) -> None:
486 _init_repo(tmp_path)
487 cid = _make_commit(tmp_path, content=b"exact-flag")
488 _make_tag(tmp_path, "v1.0", cid)
489 result = _invoke(["describe", "--exact-match", "--json"], _env(tmp_path))
490 assert result.exit_code == 0
491 data = _parse_json(result)
492 assert data["exact"] is True
493
494
495 def test_flag_exact_match_off_tag_fails(tmp_path: pathlib.Path) -> None:
496 _init_repo(tmp_path)
497 c1 = _make_commit(tmp_path, content=b"em-root")
498 _make_tag(tmp_path, "v1.0", c1)
499 _make_commit(tmp_path, parent_id=c1, content=b"em-next")
500 result = _invoke(["describe", "--exact-match"], _env(tmp_path))
501 assert result.exit_code != 0
502
503
504 def test_flag_first_parent(tmp_path: pathlib.Path) -> None:
505 _init_repo(tmp_path)
506 c1 = _make_commit(tmp_path, content=b"fp-root")
507 c2 = _make_commit(tmp_path, parent_id=c1, content=b"feat-side", branch="feat")
508 _make_tag(tmp_path, "side-tag", c2)
509 c3 = _make_commit(
510 tmp_path, parent_id=c1, parent2_id=c2, content=b"fp-merge", branch="main"
511 )
512 # --first-parent should not see side-tag.
513 result = _invoke(["describe", "--first-parent", "--json"], _env(tmp_path))
514 assert result.exit_code == 0
515 data = _parse_json(result)
516 assert data["tag"] is None # side-tag not reachable via first-parent
517
518
519 def test_flag_abbrev(tmp_path: pathlib.Path) -> None:
520 _init_repo(tmp_path)
521 _make_commit(tmp_path, content=b"abbrev-flag")
522 result = _invoke(["describe", "--abbrev", "16", "--json"], _env(tmp_path))
523 assert result.exit_code == 0
524 data = _parse_json(result)
525 assert len(data["short_sha"]) == len("sha256:") + 16
526
527
528 # ---------------------------------------------------------------------------
529 # Integration
530 # ---------------------------------------------------------------------------
531
532
533 def test_integration_ref_to_branch_tip(tmp_path: pathlib.Path) -> None:
534 _init_repo(tmp_path)
535 c1 = _make_commit(tmp_path, content=b"ref-root")
536 _make_tag(tmp_path, "v10.0", c1)
537 _make_commit(tmp_path, parent_id=c1, content=b"ref-next")
538 # Describe the HEAD (which is 1 hop past the tag).
539 result = _invoke(["describe", "--json"], _env(tmp_path))
540 assert result.exit_code == 0
541 data = _parse_json(result)
542 assert data["distance"] == 1
543 assert data["tag"] == "v10.0"
544
545
546 def test_integration_long_and_match_combined(tmp_path: pathlib.Path) -> None:
547 _init_repo(tmp_path)
548 cid = _make_commit(tmp_path, content=b"combo")
549 _make_tag(tmp_path, "v5.0.0", cid)
550 result = _invoke(
551 ["describe", "--long", "--match", "v*", "--json"], _env(tmp_path)
552 )
553 assert result.exit_code == 0
554 data = _parse_json(result)
555 assert data["name"].startswith("v5.0.0-0-sha256:")
556
557
558 def test_integration_require_tag_passes_when_tag_exists(
559 tmp_path: pathlib.Path,
560 ) -> None:
561 _init_repo(tmp_path)
562 cid = _make_commit(tmp_path, content=b"req-tag")
563 _make_tag(tmp_path, "v7.0", cid)
564 result = _invoke(["describe", "--require-tag", "--json"], _env(tmp_path))
565 assert result.exit_code == 0
566
567
568 def test_integration_text_output_sanitized(tmp_path: pathlib.Path) -> None:
569 _init_repo(tmp_path)
570 cid = _make_commit(tmp_path, content=b"text-sanitize")
571 _make_tag(tmp_path, "v1.0\x1b[1mBOLD\x1b[0m", cid)
572 result = _invoke(["describe"], _env(tmp_path))
573 assert result.exit_code == 0
574 assert "\x1b[1m" not in result.output
575
576
577 # ---------------------------------------------------------------------------
578 # E2E: help output
579 # ---------------------------------------------------------------------------
580
581
582 def test_help_contains_new_flags() -> None:
583 result = _invoke(["describe", "--help"], {})
584 assert result.exit_code == 0
585 for flag in ("--match", "--exact-match", "--first-parent", "--abbrev", "--json"):
586 assert flag in result.output, f"Missing flag in help: {flag}"
587
588
589 def test_help_mentions_json_schema() -> None:
590 result = _invoke(["describe", "--help"], {})
591 assert "json" in result.output.lower()
592
593
594 # ---------------------------------------------------------------------------
595 # Stress: deep ancestry + many tags + concurrent reads
596 # ---------------------------------------------------------------------------
597
598
599 def test_stress_5000_commit_chain(tmp_path: pathlib.Path) -> None:
600 _init_repo(tmp_path)
601 prev: str | None = None
602 root_cid = ""
603 for i in range(5_000):
604 cid = _make_commit(tmp_path, parent_id=prev, content=f"s{i}".encode())
605 if i == 0:
606 root_cid = cid
607 prev = cid
608
609 _make_tag(tmp_path, "v-deep", root_cid)
610 assert prev is not None
611 r = describe_commit(tmp_path, _REPO_ID, prev)
612 assert r["tag"] == "v-deep"
613 assert r["distance"] == 4_999
614
615
616 def test_stress_200_tags_repo(tmp_path: pathlib.Path) -> None:
617 """Many tags — describe still picks the nearest one efficiently."""
618 _init_repo(tmp_path)
619 commits: list[str] = []
620 prev: str | None = None
621 for i in range(200):
622 cid = _make_commit(tmp_path, parent_id=prev, content=f"t{i}".encode())
623 commits.append(cid)
624 # Tag every 10th commit.
625 if i % 10 == 0:
626 _make_tag(tmp_path, f"v{i}.0", cid)
627 prev = cid
628
629 # HEAD is commits[-1], nearest tag is v190.0 (at commits[190]).
630 r = describe_commit(tmp_path, _REPO_ID, commits[-1])
631 assert r["tag"] == "v190.0"
632 assert r["distance"] == 9
633
634
635 def test_stress_concurrent_describe(tmp_path: pathlib.Path) -> None:
636 """Concurrent --json calls must all return consistent, valid JSON."""
637 _init_repo(tmp_path)
638 c1 = _make_commit(tmp_path, content=b"conc-root")
639 _make_tag(tmp_path, "v-conc", c1)
640 _make_commit(tmp_path, parent_id=c1, content=b"conc-next")
641
642 invoke_lock = threading.Lock()
643 errors: list[str] = []
644
645 def _worker() -> None:
646 with invoke_lock:
647 r = _invoke(["describe", "--json"], _env(tmp_path))
648 try:
649 assert r.exit_code == 0
650 data = _parse_json(r)
651 assert data["tag"] == "v-conc"
652 assert data["distance"] == 1
653 except Exception as exc:
654 errors.append(str(exc))
655
656 threads = [threading.Thread(target=_worker) for _ in range(8)]
657 for t in threads:
658 t.start()
659 for t in threads:
660 t.join()
661
662 assert errors == [], f"Concurrent failures: {errors}"
663
664
665 # ---------------------------------------------------------------------------
666 # JSON schema: duration_ms + exit_code always present
667 # ---------------------------------------------------------------------------
668
669
670 class TestJsonSchemaComplete:
671 """Every --json path includes duration_ms and exit_code."""
672
673 def test_elapsed_present_on_tag(self, tmp_path: pathlib.Path) -> None:
674 _init_repo(tmp_path)
675 cid = _make_commit(tmp_path, content=b"sc-tag")
676 _make_tag(tmp_path, "v1.0.0", cid)
677 r = _invoke(["describe", "--json"], _env(tmp_path))
678 assert r.exit_code == 0
679 raw = json.loads(r.output)
680 assert "duration_ms" in raw
681 assert "exit_code" in raw
682
683 def test_elapsed_present_no_tag(self, tmp_path: pathlib.Path) -> None:
684 _init_repo(tmp_path)
685 _make_commit(tmp_path, content=b"sc-notag")
686 r = _invoke(["describe", "--json"], _env(tmp_path))
687 assert r.exit_code == 0
688 raw = json.loads(r.output)
689 assert "duration_ms" in raw
690 assert "exit_code" in raw
691
692 def test_elapsed_present_with_distance(self, tmp_path: pathlib.Path) -> None:
693 _init_repo(tmp_path)
694 c1 = _make_commit(tmp_path, content=b"sc-dist-root")
695 _make_tag(tmp_path, "v0.1", c1)
696 _make_commit(tmp_path, parent_id=c1, content=b"sc-dist-next")
697 r = _invoke(["describe", "--json"], _env(tmp_path))
698 assert r.exit_code == 0
699 raw = json.loads(r.output)
700 assert "duration_ms" in raw
701 assert raw["exit_code"] == 0
702
703 def test_elapsed_present_long_format(self, tmp_path: pathlib.Path) -> None:
704 _init_repo(tmp_path)
705 cid = _make_commit(tmp_path, content=b"sc-long")
706 _make_tag(tmp_path, "v2.0.0", cid)
707 r = _invoke(["describe", "--long", "--json"], _env(tmp_path))
708 assert r.exit_code == 0
709 raw = json.loads(r.output)
710 assert "duration_ms" in raw
711 assert raw["exit_code"] == 0
712
713 def test_elapsed_present_with_match(self, tmp_path: pathlib.Path) -> None:
714 _init_repo(tmp_path)
715 cid = _make_commit(tmp_path, content=b"sc-match")
716 _make_tag(tmp_path, "v3.0.0", cid)
717 r = _invoke(["describe", "--match", "v*", "--json"], _env(tmp_path))
718 assert r.exit_code == 0
719 raw = json.loads(r.output)
720 assert "duration_ms" in raw
721
722 def test_elapsed_present_with_abbrev(self, tmp_path: pathlib.Path) -> None:
723 _init_repo(tmp_path)
724 _make_commit(tmp_path, content=b"sc-abbrev")
725 r = _invoke(["describe", "--abbrev", "8", "--json"], _env(tmp_path))
726 assert r.exit_code == 0
727 raw = json.loads(r.output)
728 assert "duration_ms" in raw
729 assert raw["exit_code"] == 0
730
731 def test_all_eight_base_fields_present(self, tmp_path: pathlib.Path) -> None:
732 _init_repo(tmp_path)
733 cid = _make_commit(tmp_path, content=b"sc-all")
734 _make_tag(tmp_path, "v9.0.0", cid)
735 r = _invoke(["describe", "--json"], _env(tmp_path))
736 assert r.exit_code == 0
737 raw = json.loads(r.output)
738 for field in ("commit_id", "tag", "distance", "short_sha", "name",
739 "exact", "repo_id", "branch", "duration_ms", "exit_code"):
740 assert field in raw, f"Missing field: {field}"
741
742 def test_exit_code_field_is_zero(self, tmp_path: pathlib.Path) -> None:
743 _init_repo(tmp_path)
744 cid = _make_commit(tmp_path, content=b"sc-exit")
745 _make_tag(tmp_path, "v10.0.0", cid)
746 r = _invoke(["describe", "--json"], _env(tmp_path))
747 assert r.exit_code == 0
748 raw = json.loads(r.output)
749 assert raw["exit_code"] == 0
750
751
752 # ---------------------------------------------------------------------------
753 # duration_ms: type and magnitude checks
754 # ---------------------------------------------------------------------------
755
756
757 class TestElapsedSeconds:
758 """duration_ms is a non-negative float in a reasonable range."""
759
760 def test_elapsed_is_float(self, tmp_path: pathlib.Path) -> None:
761 _init_repo(tmp_path)
762 _make_commit(tmp_path, content=b"el-float")
763 r = _invoke(["describe", "--json"], _env(tmp_path))
764 raw = json.loads(r.output)
765 assert isinstance(raw["duration_ms"], float)
766
767 def test_elapsed_non_negative(self, tmp_path: pathlib.Path) -> None:
768 _init_repo(tmp_path)
769 _make_commit(tmp_path, content=b"el-nonneg")
770 r = _invoke(["describe", "--json"], _env(tmp_path))
771 raw = json.loads(r.output)
772 assert raw["duration_ms"] >= 0.0
773
774 def test_elapsed_under_ten_seconds(self, tmp_path: pathlib.Path) -> None:
775 _init_repo(tmp_path)
776 _make_commit(tmp_path, content=b"el-under")
777 r = _invoke(["describe", "--json"], _env(tmp_path))
778 raw = json.loads(r.output)
779 assert raw["duration_ms"] < 10.0
780
781 def test_elapsed_with_tag(self, tmp_path: pathlib.Path) -> None:
782 _init_repo(tmp_path)
783 cid = _make_commit(tmp_path, content=b"el-tag")
784 _make_tag(tmp_path, "v1.2.3", cid)
785 r = _invoke(["describe", "--json"], _env(tmp_path))
786 raw = json.loads(r.output)
787 assert raw["duration_ms"] >= 0.0
788
789 def test_elapsed_with_require_tag(self, tmp_path: pathlib.Path) -> None:
790 _init_repo(tmp_path)
791 cid = _make_commit(tmp_path, content=b"el-req")
792 _make_tag(tmp_path, "v1.0", cid)
793 r = _invoke(["describe", "--require-tag", "--json"], _env(tmp_path))
794 assert r.exit_code == 0
795 raw = json.loads(r.output)
796 assert "duration_ms" in raw
797
798 def test_elapsed_six_decimal_places(self, tmp_path: pathlib.Path) -> None:
799 _init_repo(tmp_path)
800 _make_commit(tmp_path, content=b"el-prec")
801 r = _invoke(["describe", "--json"], _env(tmp_path))
802 raw = json.loads(r.output)
803 # round(..., 6) produces at most 6 decimal places — str check
804 s = str(raw["duration_ms"])
805 dec = s.split(".")[-1] if "." in s else ""
806 assert len(dec) <= 6
807
808
809 # ---------------------------------------------------------------------------
810 # exit_code field
811 # ---------------------------------------------------------------------------
812
813
814 class TestExitCode:
815 """exit_code field mirrors process exit code; always 0 on success."""
816
817 def test_exit_code_zero_no_tag(self, tmp_path: pathlib.Path) -> None:
818 _init_repo(tmp_path)
819 _make_commit(tmp_path, content=b"ec-notag")
820 r = _invoke(["describe", "--json"], _env(tmp_path))
821 assert r.exit_code == 0
822 assert json.loads(r.output)["exit_code"] == 0
823
824 def test_exit_code_zero_on_tag(self, tmp_path: pathlib.Path) -> None:
825 _init_repo(tmp_path)
826 cid = _make_commit(tmp_path, content=b"ec-tag")
827 _make_tag(tmp_path, "v1.0.0", cid)
828 r = _invoke(["describe", "--json"], _env(tmp_path))
829 assert r.exit_code == 0
830 assert json.loads(r.output)["exit_code"] == 0
831
832 def test_exit_code_zero_with_distance(self, tmp_path: pathlib.Path) -> None:
833 _init_repo(tmp_path)
834 c1 = _make_commit(tmp_path, content=b"ec-dist-root")
835 _make_tag(tmp_path, "v0.5", c1)
836 _make_commit(tmp_path, parent_id=c1, content=b"ec-dist-next")
837 r = _invoke(["describe", "--json"], _env(tmp_path))
838 assert r.exit_code == 0
839 assert json.loads(r.output)["exit_code"] == 0
840
841 def test_exit_code_zero_long_format(self, tmp_path: pathlib.Path) -> None:
842 _init_repo(tmp_path)
843 cid = _make_commit(tmp_path, content=b"ec-long")
844 _make_tag(tmp_path, "v2.0.0", cid)
845 r = _invoke(["describe", "--long", "--json"], _env(tmp_path))
846 assert r.exit_code == 0
847 assert json.loads(r.output)["exit_code"] == 0
848
849 def test_exit_code_zero_with_abbrev(self, tmp_path: pathlib.Path) -> None:
850 _init_repo(tmp_path)
851 _make_commit(tmp_path, content=b"ec-abbrev")
852 r = _invoke(["describe", "--abbrev", "10", "--json"], _env(tmp_path))
853 assert r.exit_code == 0
854 assert json.loads(r.output)["exit_code"] == 0