gabriel / muse public
test_cmd_describe_hardening.py python
721 lines 25.3 KB
Raw
sha256:c0eba5ad689cec79f4a3fcdc4f5da78556cb4b8cb7b330f944634356c10379ed chore: pivot to nightly channel — bump version to 0.2.0.dev… Sonnet 5 patch 12 days ago
1 """Hardening test suite for ``muse describe``.
2
3 Coverage:
4 - Unit: describe_commit core — no tags, exact, distance, first_parent,
5 abbrev, _MAX_WALK budget, multi-tag tie-break
6 - Error routing: all user errors routed to stderr
7 - JSON schema: _DescribeJson shape, all fields present, repo_id + branch
8 - Flags: --pre, --first-parent, --abbrev, --json
9 - Integration: tag walk across merge commits, --first-parent vs full walk,
10 positional ref, --pre flag
11 - E2E: help output, combined flags
12 - Stress: 5 000-commit chain, 200-tag repo, concurrent reads
13 """
14
15 from __future__ import annotations
16
17 import datetime
18 import json
19 import pathlib
20 import threading
21 from typing import TypedDict
22 from unittest.mock import patch
23
24 import pytest
25 from tests.cli_test_helper import CliRunner, InvokeResult
26
27 from muse.core.describe import describe_commit, _MAX_WALK
28 from muse.core.object_store import write_object
29 from muse.core.ids import hash_commit, hash_snapshot
30 from muse.core.commits import (
31 CommitRecord,
32 write_commit,
33 )
34 from muse.core.snapshots import (
35 SnapshotRecord,
36 write_snapshot,
37 )
38 from muse.core.version_tags import (
39 VersionTagRecord,
40 write_version_tag,
41 )
42 from muse.core.semver import parse_semver
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 def _init_repo(path: pathlib.Path, *, domain: str = "code") -> pathlib.Path:
56 dot_muse = muse_dir(path)
57 for sub in ("commits", "snapshots", "objects", "refs/heads"):
58 (dot_muse / sub).mkdir(parents=True, exist_ok=True)
59 (dot_muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
60 (dot_muse / "repo.json").write_text(
61 json.dumps({"repo_id": _REPO_ID, "domain": domain}),
62 encoding="utf-8",
63 )
64 return path
65
66
67 def _make_commit(
68 root: pathlib.Path,
69 parent_id: str | None = None,
70 parent2_id: str | None = None,
71 content: bytes = b"data",
72 branch: str = "main",
73 ) -> str:
74 obj_id = blob_id(content)
75 write_object(root, obj_id, content)
76 manifest = {f"f_{obj_id[len('sha256:'):len('sha256:') + 8]}.txt": obj_id}
77 snap_id = hash_snapshot(manifest)
78 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
79 committed_at = datetime.datetime.now(datetime.timezone.utc)
80 parent_ids = [pid for pid in (parent_id, parent2_id) if pid is not None]
81 commit_id = hash_commit(
82 parent_ids=parent_ids,
83 snapshot_id=snap_id,
84 message="msg",
85 committed_at_iso=committed_at.isoformat(),
86 )
87 rec = CommitRecord(
88 commit_id=commit_id,
89 branch=branch,
90 snapshot_id=snap_id,
91 message="msg",
92 committed_at=committed_at,
93 parent_commit_id=parent_id,
94 parent2_commit_id=parent2_id,
95 )
96 write_commit(root, rec)
97 ref_path(root, branch).write_text(commit_id, encoding="utf-8")
98 return commit_id
99
100
101 def _make_vtag(root: pathlib.Path, tag: str, commit_id: str) -> None:
102 write_version_tag(root, VersionTagRecord(
103 tag_id=content_hash({"tag": tag, "commit_id": commit_id}),
104 repo_id=_REPO_ID,
105 tag=tag,
106 semver=parse_semver(tag),
107 commit_id=commit_id,
108 created_at=datetime.datetime.now(datetime.timezone.utc),
109 author="",
110 message="",
111 ))
112
113
114 def _env(repo: pathlib.Path) -> Manifest:
115 return {"MUSE_REPO_ROOT": str(repo)}
116
117
118 def _invoke(args: list[str], env: Manifest) -> InvokeResult:
119 return runner.invoke(None, args, env=env)
120
121
122 class _DescribeOut(TypedDict):
123 commit_id: str
124 tag: str | None
125 distance: int | None
126 short_sha: str
127 description: str | None
128 exact: bool
129 repo_id: str
130 branch: str
131 duration_ms: float
132 exit_code: int
133
134
135 def _parse_json(result: InvokeResult) -> _DescribeOut:
136 raw = json.loads(result.output.strip())
137 return _DescribeOut(
138 commit_id=raw["commit_id"],
139 tag=raw["tag"],
140 distance=raw["distance"],
141 short_sha=raw["short_sha"],
142 description=raw["description"],
143 exact=raw["exact"],
144 repo_id=raw["repo_id"],
145 branch=raw["branch"],
146 duration_ms=raw["duration_ms"],
147 exit_code=raw["exit_code"],
148 )
149
150
151 # ---------------------------------------------------------------------------
152 # Unit: describe_commit core
153 # ---------------------------------------------------------------------------
154
155
156 def test_core_no_tags_returns_none_description(tmp_path: pathlib.Path) -> None:
157 _init_repo(tmp_path)
158 cid = _make_commit(tmp_path, content=b"a")
159 r = describe_commit(tmp_path, _REPO_ID, cid)
160 assert r["tag"] is None
161 assert r["distance"] is None
162 assert r["description"] is None
163 assert r["exact"] is False
164 assert len(r["short_sha"]) == 8
165 assert not r["short_sha"].startswith("sha256:")
166
167
168 def test_core_exact_tag(tmp_path: pathlib.Path) -> None:
169 _init_repo(tmp_path)
170 cid = _make_commit(tmp_path, content=b"b")
171 _make_vtag(tmp_path, "v1.0.0", cid)
172 r = describe_commit(tmp_path, _REPO_ID, cid)
173 assert r["tag"] == "v1.0.0"
174 assert r["distance"] == 0
175 assert r["exact"] is True
176 assert r["description"] == "v1.0.0"
177
178
179 def test_core_distance_one(tmp_path: pathlib.Path) -> None:
180 _init_repo(tmp_path)
181 c1 = _make_commit(tmp_path, content=b"c1")
182 _make_vtag(tmp_path, "v0.9.0", c1)
183 c2 = _make_commit(tmp_path, parent_id=c1, content=b"c2")
184 r = describe_commit(tmp_path, _REPO_ID, c2)
185 assert r["tag"] == "v0.9.0"
186 assert r["distance"] == 1
187 assert r["exact"] is False
188 assert r["description"].startswith("v0.9.0-1-")
189
190
191 def test_core_abbrev_controls_sha_length(tmp_path: pathlib.Path) -> None:
192 _init_repo(tmp_path)
193 cid = _make_commit(tmp_path, content=b"abbrev")
194 r = describe_commit(tmp_path, _REPO_ID, cid, abbrev=12)
195 assert len(r["short_sha"]) == 12
196 assert not r["short_sha"].startswith("sha256:")
197
198
199 def test_core_default_abbrev_is_8(tmp_path: pathlib.Path) -> None:
200 _init_repo(tmp_path)
201 cid = _make_commit(tmp_path, content=b"abbrev-default")
202 r = describe_commit(tmp_path, _REPO_ID, cid)
203 assert len(r["short_sha"]) == 8
204
205
206 def test_core_excludes_prerelease_by_default(tmp_path: pathlib.Path) -> None:
207 _init_repo(tmp_path)
208 cid = _make_commit(tmp_path, content=b"pre")
209 _make_vtag(tmp_path, "v1.0.0-rc1", cid)
210 r = describe_commit(tmp_path, _REPO_ID, cid, include_pre=False)
211 assert r["tag"] is None
212
213
214 def test_core_includes_prerelease_with_flag(tmp_path: pathlib.Path) -> None:
215 _init_repo(tmp_path)
216 cid = _make_commit(tmp_path, content=b"pre-inc")
217 _make_vtag(tmp_path, "v1.0.0-beta1", cid)
218 r = describe_commit(tmp_path, _REPO_ID, cid, include_pre=True)
219 assert r["tag"] == "v1.0.0-beta1"
220 assert r["distance"] == 0
221
222
223 def test_core_first_parent_skips_merge_branch(tmp_path: pathlib.Path) -> None:
224 """With first_parent=True the tag on a merged branch is invisible."""
225 _init_repo(tmp_path)
226 c1 = _make_commit(tmp_path, content=b"root")
227 c2 = _make_commit(tmp_path, parent_id=c1, content=b"feat", branch="feat")
228 _make_vtag(tmp_path, "v1.0.0", c2)
229 c3 = _make_commit(
230 tmp_path, parent_id=c1, parent2_id=c2, content=b"merge", branch="main"
231 )
232 r_full = describe_commit(tmp_path, _REPO_ID, c3)
233 r_fp = describe_commit(tmp_path, _REPO_ID, c3, first_parent=True)
234 assert r_fp["tag"] is None
235 assert r_full["tag"] == "v1.0.0"
236
237
238 def test_core_multi_tag_same_commit_highest_semver_wins(tmp_path: pathlib.Path) -> None:
239 """When multiple tags point at the same commit, highest semver wins."""
240 _init_repo(tmp_path)
241 cid = _make_commit(tmp_path, content=b"multi")
242 _make_vtag(tmp_path, "v1.0.0", cid)
243 _make_vtag(tmp_path, "v2.0.0", cid)
244 _make_vtag(tmp_path, "v1.5.0", cid)
245 r = describe_commit(tmp_path, _REPO_ID, cid)
246 assert r["tag"] == "v2.0.0"
247
248
249 def test_core_max_walk_budget(tmp_path: pathlib.Path) -> None:
250 """Walk stops at _MAX_WALK without crashing; returns null tag."""
251 _init_repo(tmp_path)
252 cid = _make_commit(tmp_path, content=b"budget")
253 _make_vtag(tmp_path, "v0.0.1", cid)
254
255 call_count = 0
256
257 import muse.core.describe as _describe_mod
258 from muse.core.commits import CommitRecord as _CR
259 import datetime as _dt
260
261 def _fake_read(root: pathlib.Path, cid: str) -> "_CR | None":
262 nonlocal call_count
263 call_count += 1
264 if call_count > _MAX_WALK + 5:
265 return None
266 fake_parent = fake_id(cid)
267 return _CR(
268 commit_id=cid,
269 branch="main",
270 snapshot_id="snap",
271 message="x",
272 committed_at=_dt.datetime.now(_dt.timezone.utc),
273 parent_commit_id=fake_parent,
274 )
275
276 with patch.object(_describe_mod, "read_commit", side_effect=_fake_read):
277 far_commit = blob_id(b"far")
278 r = describe_commit(tmp_path, _REPO_ID, far_commit)
279
280 assert r["tag"] is None
281
282
283 # ---------------------------------------------------------------------------
284 # Error routing: all user errors go to stderr
285 # ---------------------------------------------------------------------------
286
287
288 def test_no_commits_error_on_stderr(tmp_path: pathlib.Path) -> None:
289 _init_repo(tmp_path)
290 result = _invoke(["describe"], _env(tmp_path))
291 assert result.exit_code != 0
292
293
294 def test_positional_ref_not_found_error(tmp_path: pathlib.Path) -> None:
295 _init_repo(tmp_path)
296 _make_commit(tmp_path, content=b"x")
297 result = _invoke(["describe", "nonexistent-branch"], _env(tmp_path))
298 assert result.exit_code != 0
299
300
301 def test_abbrev_too_small_error(tmp_path: pathlib.Path) -> None:
302 _init_repo(tmp_path)
303 _make_commit(tmp_path, content=b"ab")
304 result = _invoke(["describe", "--abbrev", "2"], _env(tmp_path))
305 assert result.exit_code != 0
306
307
308 def test_abbrev_too_large_error(tmp_path: pathlib.Path) -> None:
309 _init_repo(tmp_path)
310 _make_commit(tmp_path, content=b"ab")
311 result = _invoke(["describe", "--abbrev", "65"], _env(tmp_path))
312 assert result.exit_code != 0
313
314
315 # ---------------------------------------------------------------------------
316 # JSON schema: _DescribeJson
317 # ---------------------------------------------------------------------------
318
319
320 def test_json_schema_all_fields(tmp_path: pathlib.Path) -> None:
321 _init_repo(tmp_path)
322 cid = _make_commit(tmp_path, content=b"schema")
323 _make_vtag(tmp_path, "v1.0.0", cid)
324 result = _invoke(["describe", "--json"], _env(tmp_path))
325 assert result.exit_code == 0
326 data = _parse_json(result)
327 assert data["tag"] == "v1.0.0"
328 assert data["distance"] == 0
329 assert data["exact"] is True
330 assert data["repo_id"] == _REPO_ID
331 assert data["branch"] == "main"
332 assert data["commit_id"] == cid
333 assert data["description"] == "v1.0.0"
334
335
336 def test_json_schema_no_tag_exits_1(tmp_path: pathlib.Path) -> None:
337 _init_repo(tmp_path)
338 _make_commit(tmp_path, content=b"no-tag-json")
339 result = _invoke(["describe", "--json"], _env(tmp_path))
340 assert result.exit_code == 1
341 data = _parse_json(result)
342 assert data["tag"] is None
343 assert data["distance"] is None
344 assert data["description"] is None
345 assert data["exact"] is False
346
347
348 def test_json_schema_with_distance(tmp_path: pathlib.Path) -> None:
349 _init_repo(tmp_path)
350 c1 = _make_commit(tmp_path, content=b"root")
351 _make_vtag(tmp_path, "v0.1.0", c1)
352 _make_commit(tmp_path, parent_id=c1, content=b"next")
353 result = _invoke(["describe", "--json"], _env(tmp_path))
354 assert result.exit_code == 0
355 data = _parse_json(result)
356 assert data["tag"] == "v0.1.0"
357 assert data["distance"] == 1
358 assert data["exact"] is False
359
360
361 def test_json_abbrev_reflected(tmp_path: pathlib.Path) -> None:
362 _init_repo(tmp_path)
363 _make_commit(tmp_path, content=b"abbrev-json")
364 result = _invoke(["describe", "--abbrev", "16", "--json"], _env(tmp_path))
365 assert result.exit_code == 1
366 data = _parse_json(result)
367 assert not data["short_sha"].startswith("sha256:")
368 assert len(data["short_sha"]) == 16
369
370
371 # ---------------------------------------------------------------------------
372 # Flags: --pre, --first-parent, --abbrev
373 # ---------------------------------------------------------------------------
374
375
376 def test_flag_pre_includes_prerelease(tmp_path: pathlib.Path) -> None:
377 _init_repo(tmp_path)
378 cid = _make_commit(tmp_path, content=b"pre-flag")
379 _make_vtag(tmp_path, "v1.0.0-rc2", cid)
380 result = _invoke(["describe", "--pre", "--json"], _env(tmp_path))
381 assert result.exit_code == 0
382 data = _parse_json(result)
383 assert data["tag"] == "v1.0.0-rc2"
384
385
386 def test_flag_pre_absent_excludes_prerelease(tmp_path: pathlib.Path) -> None:
387 _init_repo(tmp_path)
388 cid = _make_commit(tmp_path, content=b"pre-absent")
389 _make_vtag(tmp_path, "v1.0.0-alpha1", cid)
390 result = _invoke(["describe", "--json"], _env(tmp_path))
391 assert result.exit_code == 1
392 data = _parse_json(result)
393 assert data["tag"] is None
394
395
396 def test_flag_first_parent(tmp_path: pathlib.Path) -> None:
397 _init_repo(tmp_path)
398 c1 = _make_commit(tmp_path, content=b"fp-root")
399 c2 = _make_commit(tmp_path, parent_id=c1, content=b"feat-side", branch="feat")
400 _make_vtag(tmp_path, "v1.0.0", c2)
401 c3 = _make_commit(
402 tmp_path, parent_id=c1, parent2_id=c2, content=b"fp-merge", branch="main"
403 )
404 result = _invoke(["describe", "--first-parent", "--json"], _env(tmp_path))
405 assert result.exit_code == 1
406 data = _parse_json(result)
407 assert data["tag"] is None
408
409
410 def test_flag_abbrev_changes_short_sha_length(tmp_path: pathlib.Path) -> None:
411 _init_repo(tmp_path)
412 cid = _make_commit(tmp_path, content=b"abbrev-flag")
413 _make_vtag(tmp_path, "v1.0.0", cid)
414 result = _invoke(["describe", "--abbrev", "16", "--json"], _env(tmp_path))
415 assert result.exit_code == 0
416 data = _parse_json(result)
417 assert len(data["short_sha"]) == 16
418 assert not data["short_sha"].startswith("sha256:")
419
420
421 # ---------------------------------------------------------------------------
422 # Integration
423 # ---------------------------------------------------------------------------
424
425
426 def test_integration_head_is_one_hop_past_tag(tmp_path: pathlib.Path) -> None:
427 _init_repo(tmp_path)
428 c1 = _make_commit(tmp_path, content=b"ref-root")
429 _make_vtag(tmp_path, "v10.0.0", c1)
430 _make_commit(tmp_path, parent_id=c1, content=b"ref-next")
431 result = _invoke(["describe", "--json"], _env(tmp_path))
432 assert result.exit_code == 0
433 data = _parse_json(result)
434 assert data["distance"] == 1
435 assert data["tag"] == "v10.0.0"
436
437
438 def test_integration_pre_flag_and_abbrev_combined(tmp_path: pathlib.Path) -> None:
439 _init_repo(tmp_path)
440 cid = _make_commit(tmp_path, content=b"combo")
441 _make_vtag(tmp_path, "v5.0.0-rc1", cid)
442 result = _invoke(["describe", "--pre", "--abbrev", "12", "--json"], _env(tmp_path))
443 assert result.exit_code == 0
444 data = _parse_json(result)
445 assert data["tag"] == "v5.0.0-rc1"
446 assert len(data["short_sha"]) == 12
447
448
449 def test_integration_positional_ref_branch(tmp_path: pathlib.Path) -> None:
450 _init_repo(tmp_path)
451 c1 = _make_commit(tmp_path, content=b"pos-root")
452 _make_vtag(tmp_path, "v3.0.0", c1)
453 result = _invoke(["describe", "main", "--json"], _env(tmp_path))
454 assert result.exit_code == 0
455 data = _parse_json(result)
456 assert data["tag"] == "v3.0.0"
457
458
459 # ---------------------------------------------------------------------------
460 # E2E: help output
461 # ---------------------------------------------------------------------------
462
463
464 def test_help_contains_expected_flags() -> None:
465 result = _invoke(["describe", "--help"], {})
466 assert result.exit_code == 0
467 for flag in ("--pre", "--first-parent", "--abbrev", "--json"):
468 assert flag in result.output, f"Missing flag in help: {flag}"
469
470
471 def test_help_mentions_json_schema() -> None:
472 result = _invoke(["describe", "--help"], {})
473 assert "json" in result.output.lower()
474
475
476 def test_help_no_removed_flags() -> None:
477 result = _invoke(["describe", "--help"], {})
478 for removed in ("--long", "--require-tag", "--exact-match", "--match", "--ref"):
479 assert removed not in result.output, f"Removed flag still in help: {removed}"
480
481
482 # ---------------------------------------------------------------------------
483 # Stress: deep ancestry + many tags + concurrent reads
484 # ---------------------------------------------------------------------------
485
486
487 def test_stress_5000_commit_chain(tmp_path: pathlib.Path) -> None:
488 _init_repo(tmp_path)
489 prev: str | None = None
490 root_cid = ""
491 for i in range(5_000):
492 cid = _make_commit(tmp_path, parent_id=prev, content=f"s{i}".encode())
493 if i == 0:
494 root_cid = cid
495 prev = cid
496
497 _make_vtag(tmp_path, "v1.0.0", root_cid)
498 assert prev is not None
499 r = describe_commit(tmp_path, _REPO_ID, prev)
500 assert r["tag"] == "v1.0.0"
501 assert r["distance"] == 4_999
502
503
504 def test_stress_200_tags_repo(tmp_path: pathlib.Path) -> None:
505 """Many tags — describe still picks the nearest one efficiently."""
506 _init_repo(tmp_path)
507 commits: list[str] = []
508 prev: str | None = None
509 for i in range(200):
510 cid = _make_commit(tmp_path, parent_id=prev, content=f"t{i}".encode())
511 commits.append(cid)
512 if i % 10 == 0:
513 _make_vtag(tmp_path, f"v{i}.0.0", cid)
514 prev = cid
515
516 r = describe_commit(tmp_path, _REPO_ID, commits[-1])
517 assert r["tag"] == "v190.0.0"
518 assert r["distance"] == 9
519
520
521 def test_stress_concurrent_describe(tmp_path: pathlib.Path) -> None:
522 """Concurrent --json calls must all return consistent, valid JSON."""
523 _init_repo(tmp_path)
524 c1 = _make_commit(tmp_path, content=b"conc-root")
525 _make_vtag(tmp_path, "v1.0.0", c1)
526 _make_commit(tmp_path, parent_id=c1, content=b"conc-next")
527
528 invoke_lock = threading.Lock()
529 errors: list[str] = []
530
531 def _worker() -> None:
532 with invoke_lock:
533 r = _invoke(["describe", "--json"], _env(tmp_path))
534 try:
535 assert r.exit_code == 0
536 data = _parse_json(r)
537 assert data["tag"] == "v1.0.0"
538 assert data["distance"] == 1
539 except Exception as exc:
540 errors.append(str(exc))
541
542 threads = [threading.Thread(target=_worker) for _ in range(8)]
543 for t in threads:
544 t.start()
545 for t in threads:
546 t.join()
547
548 assert errors == [], f"Concurrent failures: {errors}"
549
550
551 # ---------------------------------------------------------------------------
552 # JSON schema: duration_ms + exit_code always present
553 # ---------------------------------------------------------------------------
554
555
556 class TestJsonSchemaComplete:
557 """Every --json path includes duration_ms and exit_code."""
558
559 def test_elapsed_present_on_tag(self, tmp_path: pathlib.Path) -> None:
560 _init_repo(tmp_path)
561 cid = _make_commit(tmp_path, content=b"sc-tag")
562 _make_vtag(tmp_path, "v1.0.0", cid)
563 r = _invoke(["describe", "--json"], _env(tmp_path))
564 assert r.exit_code == 0
565 raw = json.loads(r.output)
566 assert "duration_ms" in raw
567 assert "exit_code" in raw
568
569 def test_elapsed_present_no_tag(self, tmp_path: pathlib.Path) -> None:
570 _init_repo(tmp_path)
571 _make_commit(tmp_path, content=b"sc-notag")
572 r = _invoke(["describe", "--json"], _env(tmp_path))
573 assert r.exit_code == 1
574 raw = json.loads(r.output)
575 assert "duration_ms" in raw
576 assert "exit_code" in raw
577
578 def test_elapsed_present_with_distance(self, tmp_path: pathlib.Path) -> None:
579 _init_repo(tmp_path)
580 c1 = _make_commit(tmp_path, content=b"sc-dist-root")
581 _make_vtag(tmp_path, "v0.1.0", c1)
582 _make_commit(tmp_path, parent_id=c1, content=b"sc-dist-next")
583 r = _invoke(["describe", "--json"], _env(tmp_path))
584 assert r.exit_code == 0
585 raw = json.loads(r.output)
586 assert "duration_ms" in raw
587 assert raw["exit_code"] == 0
588
589 def test_elapsed_present_with_abbrev(self, tmp_path: pathlib.Path) -> None:
590 _init_repo(tmp_path)
591 _make_commit(tmp_path, content=b"sc-abbrev")
592 r = _invoke(["describe", "--abbrev", "8", "--json"], _env(tmp_path))
593 assert r.exit_code == 1
594 raw = json.loads(r.output)
595 assert "duration_ms" in raw
596 assert raw["exit_code"] == 1
597
598 def test_all_base_fields_present(self, tmp_path: pathlib.Path) -> None:
599 _init_repo(tmp_path)
600 cid = _make_commit(tmp_path, content=b"sc-all")
601 _make_vtag(tmp_path, "v9.0.0", cid)
602 r = _invoke(["describe", "--json"], _env(tmp_path))
603 assert r.exit_code == 0
604 raw = json.loads(r.output)
605 for field in ("commit_id", "tag", "distance", "short_sha", "description",
606 "exact", "repo_id", "branch", "duration_ms", "exit_code"):
607 assert field in raw, f"Missing field: {field}"
608
609 def test_name_field_not_present(self, tmp_path: pathlib.Path) -> None:
610 """The old 'name' field must not appear — it was replaced by 'description'."""
611 _init_repo(tmp_path)
612 cid = _make_commit(tmp_path, content=b"sc-name")
613 _make_vtag(tmp_path, "v1.0.0", cid)
614 r = _invoke(["describe", "--json"], _env(tmp_path))
615 raw = json.loads(r.output)
616 assert "name" not in raw
617
618 def test_exit_code_field_is_zero_on_tag(self, tmp_path: pathlib.Path) -> None:
619 _init_repo(tmp_path)
620 cid = _make_commit(tmp_path, content=b"sc-exit")
621 _make_vtag(tmp_path, "v10.0.0", cid)
622 r = _invoke(["describe", "--json"], _env(tmp_path))
623 assert r.exit_code == 0
624 raw = json.loads(r.output)
625 assert raw["exit_code"] == 0
626
627 def test_exit_code_field_is_one_on_no_tag(self, tmp_path: pathlib.Path) -> None:
628 _init_repo(tmp_path)
629 _make_commit(tmp_path, content=b"sc-exit-notag")
630 r = _invoke(["describe", "--json"], _env(tmp_path))
631 assert r.exit_code == 1
632 raw = json.loads(r.output)
633 assert raw["exit_code"] == 1
634
635
636 # ---------------------------------------------------------------------------
637 # duration_ms: type and magnitude checks
638 # ---------------------------------------------------------------------------
639
640
641 class TestElapsedSeconds:
642 """duration_ms is a non-negative float in a reasonable range."""
643
644 def test_elapsed_is_float(self, tmp_path: pathlib.Path) -> None:
645 _init_repo(tmp_path)
646 _make_commit(tmp_path, content=b"el-float")
647 r = _invoke(["describe", "--json"], _env(tmp_path))
648 raw = json.loads(r.output)
649 assert isinstance(raw["duration_ms"], float)
650
651 def test_elapsed_non_negative(self, tmp_path: pathlib.Path) -> None:
652 _init_repo(tmp_path)
653 _make_commit(tmp_path, content=b"el-nonneg")
654 r = _invoke(["describe", "--json"], _env(tmp_path))
655 raw = json.loads(r.output)
656 assert raw["duration_ms"] >= 0.0
657
658 def test_elapsed_under_ten_seconds(self, tmp_path: pathlib.Path) -> None:
659 _init_repo(tmp_path)
660 _make_commit(tmp_path, content=b"el-under")
661 r = _invoke(["describe", "--json"], _env(tmp_path))
662 raw = json.loads(r.output)
663 assert raw["duration_ms"] < 10.0
664
665 def test_elapsed_with_tag(self, tmp_path: pathlib.Path) -> None:
666 _init_repo(tmp_path)
667 cid = _make_commit(tmp_path, content=b"el-tag")
668 _make_vtag(tmp_path, "v1.2.3", cid)
669 r = _invoke(["describe", "--json"], _env(tmp_path))
670 raw = json.loads(r.output)
671 assert raw["duration_ms"] >= 0.0
672
673 def test_elapsed_six_decimal_places(self, tmp_path: pathlib.Path) -> None:
674 _init_repo(tmp_path)
675 _make_commit(tmp_path, content=b"el-prec")
676 r = _invoke(["describe", "--json"], _env(tmp_path))
677 raw = json.loads(r.output)
678 s = str(raw["duration_ms"])
679 dec = s.split(".")[-1] if "." in s else ""
680 assert len(dec) <= 6
681
682
683 # ---------------------------------------------------------------------------
684 # exit_code field
685 # ---------------------------------------------------------------------------
686
687
688 class TestExitCode:
689 """exit_code field mirrors process exit code."""
690
691 def test_exit_code_one_no_tag(self, tmp_path: pathlib.Path) -> None:
692 _init_repo(tmp_path)
693 _make_commit(tmp_path, content=b"ec-notag")
694 r = _invoke(["describe", "--json"], _env(tmp_path))
695 assert r.exit_code == 1
696 assert json.loads(r.output)["exit_code"] == 1
697
698 def test_exit_code_zero_on_tag(self, tmp_path: pathlib.Path) -> None:
699 _init_repo(tmp_path)
700 cid = _make_commit(tmp_path, content=b"ec-tag")
701 _make_vtag(tmp_path, "v1.0.0", cid)
702 r = _invoke(["describe", "--json"], _env(tmp_path))
703 assert r.exit_code == 0
704 assert json.loads(r.output)["exit_code"] == 0
705
706 def test_exit_code_zero_with_distance(self, tmp_path: pathlib.Path) -> None:
707 _init_repo(tmp_path)
708 c1 = _make_commit(tmp_path, content=b"ec-dist-root")
709 _make_vtag(tmp_path, "v0.5.0", c1)
710 _make_commit(tmp_path, parent_id=c1, content=b"ec-dist-next")
711 r = _invoke(["describe", "--json"], _env(tmp_path))
712 assert r.exit_code == 0
713 assert json.loads(r.output)["exit_code"] == 0
714
715 def test_exit_code_zero_with_abbrev(self, tmp_path: pathlib.Path) -> None:
716 _init_repo(tmp_path)
717 cid = _make_commit(tmp_path, content=b"ec-abbrev")
718 _make_vtag(tmp_path, "v1.0.0", cid)
719 r = _invoke(["describe", "--abbrev", "10", "--json"], _env(tmp_path))
720 assert r.exit_code == 0
721 assert json.loads(r.output)["exit_code"] == 0
File History 10 commits
sha256:c287f599c5429903a139eadf3c5db5d930520e57cb0c3c575d9570e953c3b2d6 chore: bump version to 0.2.0.dev2 — nightly.2 Sonnet 4.6 patch 11 days ago
sha256:8de4334a98c945aace420969d389ad678aa926d4ab4e886b2ac4c4241cb3bf2b revert: keep pyproject.toml in canonical PEP 440 form Sonnet 4.6 patch 14 days ago
sha256:50bb615c573aa4b928fca75b60f82ba2a659910066507cec6a95c412ae22ccb9 docs: add issue docs for push have-negotiation bug (#55) an… Sonnet 4.6 18 days ago
sha256:d90d175cded68aae1d4ffcf4858917854195d0cd8ce1fe73cee4dbc02541cb74 chore: trigger push to surface null-OID paths Sonnet 4.6 23 days ago
sha256:e452ad9a6ace6ccc6d875a35e06caf9da5576a970c1c36133b69a891ce5fefa8 chore: prebuild timing test Sonnet 4.6 33 days ago
sha256:0008ab6695e3e064b3e236b24fd19e538fef6a588eb0d211622f4466d919c0b1 merge: pull staging/dev — advance to 0.2.0rc12 Sonnet 4.6 patch 35 days ago
sha256:9c33d61749fff814c5226d5386aa2af7064c2c02788594a25fdd709358132eea fix: _PROPOSAL_PREFIX_RESOLVE_LIMIT 200 → 100 to match hub … Sonnet 4.6 47 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 49 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 56 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 56 days ago