gabriel / muse public
test_annotate_command.py python
972 lines 37.6 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 22 days ago
1 """Tests for muse annotate — CRDT-backed commit annotations.
2
3 Tiers:
4 1. Unit — validators and helpers in isolation (no repo, no CLI)
5 2. Integration — store round-trip: write → annotate → read_commit
6 3. End-to-End — full CLI invocations via CliRunner
7 4. Security — injection, control chars, oversized inputs, path traversal
8 5. Stress — many sequential annotations, large inputs at limits
9 6. Performance — timing assertions on hot paths
10 7. Data Integrity — CRDT semantics (ORSet idempotency, GCounter monotone,
11 LWW last-write, append-only notes, roundtrip fidelity)
12 """
13
14 from __future__ import annotations
15
16 import datetime
17 import json
18 import pathlib
19 import time
20
21 import pytest
22 from tests.cli_test_helper import CliRunner
23
24 cli = None # argparse migration — CliRunner ignores this arg
25
26 from muse.cli.commands.annotate import (
27 _MAX_LABEL_LEN,
28 _MAX_NOTE_LEN,
29 _MAX_REVIEWER_LEN,
30 _STATUS_VALUES,
31 _validate_label,
32 _validate_note,
33 _validate_reviewer,
34 _validate_score,
35 _validate_status,
36 )
37 from muse.core.ids import hash_commit, hash_snapshot
38 from muse.core.commits import (
39 CommitRecord,
40 read_commit,
41 write_commit,
42 )
43 from muse.core.paths import heads_dir, muse_dir
44
45 runner = CliRunner()
46
47
48 # ---------------------------------------------------------------------------
49 # Shared fixtures
50 # ---------------------------------------------------------------------------
51
52
53 @pytest.fixture
54 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
55 """Minimal Muse repo with a single commit on main."""
56 monkeypatch.chdir(tmp_path)
57 dot_muse = muse_dir(tmp_path)
58 dot_muse.mkdir()
59 (dot_muse / "repo.json").write_text('{"repo_id":"test-repo"}')
60 (dot_muse / "HEAD").write_text("ref: refs/heads/main")
61 (dot_muse / "commits").mkdir()
62 (dot_muse / "snapshots").mkdir()
63 (dot_muse / "refs" / "heads").mkdir(parents=True)
64 return tmp_path
65
66
67 def _write_commit(
68 root: pathlib.Path,
69 message: str = "test commit",
70 *,
71 parent: str | None = None,
72 ) -> CommitRecord:
73 """Write a content-addressed CommitRecord and update the branch ref."""
74 committed_at = datetime.datetime(2026, 3, 1, tzinfo=datetime.timezone.utc)
75 snap_id = hash_snapshot({})
76 parents = [parent] if parent else []
77 cid = hash_commit( parent_ids=parents,
78 snapshot_id=snap_id,
79 message=message,
80 committed_at_iso=committed_at.isoformat(),
81 author="test-author",
82 )
83 record = CommitRecord(
84 commit_id=cid,
85 branch="main",
86 snapshot_id=snap_id,
87 message=message,
88 committed_at=committed_at,
89 author="test-author",
90 parent_commit_id=parent,
91 )
92 write_commit(root, record)
93 (heads_dir(root) / "main").write_text(cid)
94 return record
95
96
97 # ===========================================================================
98 # 1. Unit tests — validators only, no repo, no I/O
99 # ===========================================================================
100
101
102 class TestValidateReviewer:
103 def test_valid_name_passes(self) -> None:
104 assert _validate_reviewer("alice") == "alice"
105
106 def test_valid_agent_id_passes(self) -> None:
107 assert _validate_reviewer("claude-opus-4") == "claude-opus-4"
108
109 def test_empty_name_exits(self) -> None:
110 with pytest.raises(SystemExit):
111 _validate_reviewer("")
112
113 def test_name_at_max_len_passes(self) -> None:
114 name = "a" * _MAX_REVIEWER_LEN
115 assert _validate_reviewer(name) == name
116
117 def test_name_over_max_len_exits(self) -> None:
118 with pytest.raises(SystemExit):
119 _validate_reviewer("a" * (_MAX_REVIEWER_LEN + 1))
120
121 def test_control_char_exits(self) -> None:
122 with pytest.raises(SystemExit):
123 _validate_reviewer("alice\x00")
124
125 def test_ansi_escape_exits(self) -> None:
126 with pytest.raises(SystemExit):
127 _validate_reviewer("alice\x1b[31m")
128
129 def test_newline_exits(self) -> None:
130 with pytest.raises(SystemExit):
131 _validate_reviewer("alice\nbob")
132
133
134 class TestValidateLabel:
135 def test_valid_label_passes(self) -> None:
136 assert _validate_label("hotfix") == "hotfix"
137
138 def test_label_at_max_len_passes(self) -> None:
139 lbl = "x" * _MAX_LABEL_LEN
140 assert _validate_label(lbl) == lbl
141
142 def test_label_over_max_len_exits(self) -> None:
143 with pytest.raises(SystemExit):
144 _validate_label("x" * (_MAX_LABEL_LEN + 1))
145
146 def test_empty_label_exits(self) -> None:
147 with pytest.raises(SystemExit):
148 _validate_label("")
149
150 def test_control_char_exits(self) -> None:
151 with pytest.raises(SystemExit):
152 _validate_label("hot\x01fix")
153
154
155 class TestValidateStatus:
156 def test_all_valid_statuses_pass(self) -> None:
157 for s in _STATUS_VALUES:
158 assert _validate_status(s) == s
159
160 def test_empty_string_clears(self) -> None:
161 assert _validate_status("") == ""
162
163 def test_unknown_status_exits(self) -> None:
164 with pytest.raises(SystemExit):
165 _validate_status("unknown-state")
166
167 def test_case_sensitive(self) -> None:
168 with pytest.raises(SystemExit):
169 _validate_status("Approved")
170
171
172 class TestValidateScore:
173 def test_zero_passes(self) -> None:
174 assert _validate_score("0.0") == 0.0
175
176 def test_one_passes(self) -> None:
177 assert _validate_score("1.0") == 1.0
178
179 def test_midpoint_passes(self) -> None:
180 assert _validate_score("0.5") == pytest.approx(0.5)
181
182 def test_below_zero_exits(self) -> None:
183 with pytest.raises(SystemExit):
184 _validate_score("-0.1")
185
186 def test_above_one_exits(self) -> None:
187 with pytest.raises(SystemExit):
188 _validate_score("1.1")
189
190 def test_non_numeric_exits(self) -> None:
191 with pytest.raises(SystemExit):
192 _validate_score("high")
193
194 def test_integer_string_passes(self) -> None:
195 assert _validate_score("1") == 1.0
196
197
198 class TestValidateNote:
199 def test_valid_note_passes(self) -> None:
200 assert _validate_note("all good") == "all good"
201
202 def test_empty_exits(self) -> None:
203 with pytest.raises(SystemExit):
204 _validate_note("")
205
206 def test_whitespace_only_exits(self) -> None:
207 with pytest.raises(SystemExit):
208 _validate_note(" ")
209
210 def test_note_at_max_len_passes(self) -> None:
211 note = "a" * _MAX_NOTE_LEN
212 assert _validate_note(note) == note
213
214 def test_note_over_max_len_exits(self) -> None:
215 with pytest.raises(SystemExit):
216 _validate_note("a" * (_MAX_NOTE_LEN + 1))
217
218
219 # ===========================================================================
220 # 2. Integration tests — store round-trip
221 # ===========================================================================
222
223
224 class TestStoreRoundTrip:
225 def test_reviewed_by_persisted(self, repo: pathlib.Path) -> None:
226 c = _write_commit(repo)
227 runner.invoke(
228 cli, ["annotate", "--reviewed-by", "alice", c.commit_id],
229 catch_exceptions=False,
230 )
231 stored = read_commit(repo, c.commit_id)
232 assert stored is not None
233 assert "alice" in stored.reviewed_by
234
235 def test_test_runs_persisted(self, repo: pathlib.Path) -> None:
236 c = _write_commit(repo)
237 runner.invoke(cli, ["annotate", "--test-run", c.commit_id], catch_exceptions=False)
238 stored = read_commit(repo, c.commit_id)
239 assert stored is not None
240 assert stored.test_runs == 1
241
242 def test_labels_persisted(self, repo: pathlib.Path) -> None:
243 c = _write_commit(repo)
244 runner.invoke(cli, ["annotate", "--label", "hotfix", c.commit_id], catch_exceptions=False)
245 stored = read_commit(repo, c.commit_id)
246 assert stored is not None
247 assert "hotfix" in stored.labels
248
249 def test_status_persisted(self, repo: pathlib.Path) -> None:
250 c = _write_commit(repo)
251 runner.invoke(cli, ["annotate", "--status", "approved", c.commit_id], catch_exceptions=False)
252 stored = read_commit(repo, c.commit_id)
253 assert stored is not None
254 assert stored.status == "approved"
255
256 def test_notes_persisted(self, repo: pathlib.Path) -> None:
257 c = _write_commit(repo)
258 runner.invoke(
259 cli, ["annotate", "--note", "looks good", c.commit_id],
260 catch_exceptions=False,
261 )
262 stored = read_commit(repo, c.commit_id)
263 assert stored is not None
264 assert "looks good" in stored.notes
265
266 def test_score_persisted(self, repo: pathlib.Path) -> None:
267 c = _write_commit(repo)
268 runner.invoke(cli, ["annotate", "--score", "0.9", c.commit_id], catch_exceptions=False)
269 stored = read_commit(repo, c.commit_id)
270 assert stored is not None
271 assert stored.score == pytest.approx(0.9)
272
273 def test_all_fields_in_one_call(self, repo: pathlib.Path) -> None:
274 c = _write_commit(repo)
275 runner.invoke(
276 cli,
277 [
278 "annotate",
279 "--reviewed-by", "alice",
280 "--test-run",
281 "--label", "perf",
282 "--status", "pending",
283 "--note", "first review",
284 "--score", "0.75",
285 c.commit_id,
286 ],
287 catch_exceptions=False,
288 )
289 stored = read_commit(repo, c.commit_id)
290 assert stored is not None
291 assert "alice" in stored.reviewed_by
292 assert stored.test_runs == 1
293 assert "perf" in stored.labels
294 assert stored.status == "pending"
295 assert "first review" in stored.notes
296 assert stored.score == pytest.approx(0.75)
297
298 def test_dry_run_does_not_write(self, repo: pathlib.Path) -> None:
299 c = _write_commit(repo)
300 runner.invoke(
301 cli,
302 ["annotate", "--dry-run", "--reviewed-by", "agent-x", c.commit_id],
303 catch_exceptions=False,
304 )
305 stored = read_commit(repo, c.commit_id)
306 assert stored is not None
307 assert "agent-x" not in stored.reviewed_by
308
309
310 # ===========================================================================
311 # 3. End-to-End tests — CLI invocations
312 # ===========================================================================
313
314
315 class TestShowMode:
316 def test_show_no_flags_exits_0(self, repo: pathlib.Path) -> None:
317 c = _write_commit(repo)
318 result = runner.invoke(cli, ["annotate", c.commit_id], catch_exceptions=False)
319 assert result.exit_code == 0
320
321 def test_show_includes_reviewed_by_header(self, repo: pathlib.Path) -> None:
322 c = _write_commit(repo)
323 result = runner.invoke(cli, ["annotate", c.commit_id], catch_exceptions=False)
324 assert "reviewed-by" in result.output
325
326 def test_show_includes_test_runs_header(self, repo: pathlib.Path) -> None:
327 c = _write_commit(repo)
328 result = runner.invoke(cli, ["annotate", c.commit_id], catch_exceptions=False)
329 assert "test-runs" in result.output
330
331 def test_show_includes_labels_header(self, repo: pathlib.Path) -> None:
332 c = _write_commit(repo)
333 result = runner.invoke(cli, ["annotate", c.commit_id], catch_exceptions=False)
334 assert "labels" in result.output
335
336 def test_show_includes_status_header(self, repo: pathlib.Path) -> None:
337 c = _write_commit(repo)
338 result = runner.invoke(cli, ["annotate", c.commit_id], catch_exceptions=False)
339 assert "status" in result.output
340
341 def test_show_includes_notes_header(self, repo: pathlib.Path) -> None:
342 c = _write_commit(repo)
343 result = runner.invoke(cli, ["annotate", c.commit_id], catch_exceptions=False)
344 assert "notes" in result.output
345
346 def test_show_includes_score_header(self, repo: pathlib.Path) -> None:
347 c = _write_commit(repo)
348 result = runner.invoke(cli, ["annotate", c.commit_id], catch_exceptions=False)
349 assert "score" in result.output
350
351 def test_show_head_when_no_commit_arg(self, repo: pathlib.Path) -> None:
352 _write_commit(repo)
353 result = runner.invoke(cli, ["annotate"], catch_exceptions=False)
354 assert result.exit_code == 0
355
356
357 class TestJsonOutput:
358 def test_json_flag_exits_0(self, repo: pathlib.Path) -> None:
359 c = _write_commit(repo)
360 result = runner.invoke(
361 cli, ["annotate", "--json", c.commit_id], catch_exceptions=False
362 )
363 assert result.exit_code == 0
364
365 def test_json_is_valid(self, repo: pathlib.Path) -> None:
366 c = _write_commit(repo)
367 result = runner.invoke(
368 cli, ["annotate", "--json", c.commit_id], catch_exceptions=False
369 )
370 data = json.loads(result.output)
371 assert isinstance(data, dict)
372
373 def test_json_has_all_keys(self, repo: pathlib.Path) -> None:
374 c = _write_commit(repo)
375 result = runner.invoke(
376 cli, ["annotate", "--json", c.commit_id], catch_exceptions=False
377 )
378 data = json.loads(result.output)
379 required = {
380 "commit_id", "parent_commit_id", "snapshot_id",
381 "message", "branch", "author", "agent_id", "model_id",
382 "committed_at", "reviewed_by", "test_runs",
383 "labels", "status", "notes", "score",
384 "changed", "dry_run",
385 }
386 assert required <= data.keys()
387
388 def test_json_commit_id_matches(self, repo: pathlib.Path) -> None:
389 c = _write_commit(repo)
390 result = runner.invoke(
391 cli, ["annotate", "--json", c.commit_id], catch_exceptions=False
392 )
393 data = json.loads(result.output)
394 assert data["commit_id"] == c.commit_id
395
396 def test_json_mutation_reflects_new_values(self, repo: pathlib.Path) -> None:
397 c = _write_commit(repo)
398 result = runner.invoke(
399 cli,
400 ["annotate", "--json", "--reviewed-by", "bob", "--score", "0.8", c.commit_id],
401 catch_exceptions=False,
402 )
403 data = json.loads(result.output)
404 assert "bob" in data["reviewed_by"]
405 assert data["score"] == pytest.approx(0.8)
406 assert data["changed"] is True
407 assert data["dry_run"] is False
408
409 def test_json_dry_run_flag(self, repo: pathlib.Path) -> None:
410 c = _write_commit(repo)
411 result = runner.invoke(
412 cli,
413 ["annotate", "--json", "--dry-run", "--reviewed-by", "alice", c.commit_id],
414 catch_exceptions=False,
415 )
416 data = json.loads(result.output)
417 assert data["dry_run"] is True
418 assert "alice" in data["reviewed_by"]
419
420 def test_json_snapshot_id_present(self, repo: pathlib.Path) -> None:
421 c = _write_commit(repo)
422 result = runner.invoke(
423 cli, ["annotate", "--json", c.commit_id], catch_exceptions=False
424 )
425 data = json.loads(result.output)
426 assert data["snapshot_id"] == c.snapshot_id
427
428 def test_json_parent_commit_id_null_for_root(self, repo: pathlib.Path) -> None:
429 c = _write_commit(repo)
430 result = runner.invoke(
431 cli, ["annotate", "--json", c.commit_id], catch_exceptions=False
432 )
433 data = json.loads(result.output)
434 assert data["parent_commit_id"] is None
435
436
437 class TestReviewerFlags:
438 def test_add_single_reviewer(self, repo: pathlib.Path) -> None:
439 c = _write_commit(repo)
440 result = runner.invoke(
441 cli, ["annotate", "--reviewed-by", "agent-x", c.commit_id],
442 catch_exceptions=False,
443 )
444 assert result.exit_code == 0
445 assert "agent-x" in result.output
446
447 def test_add_comma_separated_reviewers(self, repo: pathlib.Path) -> None:
448 c = _write_commit(repo)
449 runner.invoke(
450 cli, ["annotate", "--reviewed-by", "alice,bob", c.commit_id],
451 catch_exceptions=False,
452 )
453 stored = read_commit(repo, c.commit_id)
454 assert stored is not None
455 assert "alice" in stored.reviewed_by
456 assert "bob" in stored.reviewed_by
457
458 def test_add_multi_flag_reviewers(self, repo: pathlib.Path) -> None:
459 c = _write_commit(repo)
460 runner.invoke(
461 cli,
462 ["annotate", "--reviewed-by", "alice", "--reviewed-by", "bob", c.commit_id],
463 catch_exceptions=False,
464 )
465 stored = read_commit(repo, c.commit_id)
466 assert stored is not None
467 assert "alice" in stored.reviewed_by
468 assert "bob" in stored.reviewed_by
469
470 def test_remove_reviewer(self, repo: pathlib.Path) -> None:
471 c = _write_commit(repo)
472 runner.invoke(cli, ["annotate", "--reviewed-by", "alice", c.commit_id], catch_exceptions=False)
473 runner.invoke(cli, ["annotate", "--remove-reviewer", "alice", c.commit_id], catch_exceptions=False)
474 stored = read_commit(repo, c.commit_id)
475 assert stored is not None
476 assert "alice" not in stored.reviewed_by
477
478 def test_remove_nonexistent_reviewer_warns(self, repo: pathlib.Path) -> None:
479 c = _write_commit(repo)
480 result = runner.invoke(
481 cli, ["annotate", "--remove-reviewer", "nobody", c.commit_id],
482 catch_exceptions=False,
483 )
484 assert result.exit_code == 0
485
486
487 class TestLabelFlags:
488 def test_add_single_label(self, repo: pathlib.Path) -> None:
489 c = _write_commit(repo)
490 result = runner.invoke(
491 cli, ["annotate", "--label", "hotfix", c.commit_id],
492 catch_exceptions=False,
493 )
494 assert result.exit_code == 0
495 assert "hotfix" in result.output
496
497 def test_add_comma_separated_labels(self, repo: pathlib.Path) -> None:
498 c = _write_commit(repo)
499 runner.invoke(
500 cli, ["annotate", "--label", "hotfix,perf", c.commit_id],
501 catch_exceptions=False,
502 )
503 stored = read_commit(repo, c.commit_id)
504 assert stored is not None
505 assert "hotfix" in stored.labels
506 assert "perf" in stored.labels
507
508 def test_remove_label(self, repo: pathlib.Path) -> None:
509 c = _write_commit(repo)
510 runner.invoke(cli, ["annotate", "--label", "hotfix", c.commit_id], catch_exceptions=False)
511 runner.invoke(cli, ["annotate", "--remove-label", "hotfix", c.commit_id], catch_exceptions=False)
512 stored = read_commit(repo, c.commit_id)
513 assert stored is not None
514 assert "hotfix" not in stored.labels
515
516 def test_remove_nonexistent_label_warns(self, repo: pathlib.Path) -> None:
517 c = _write_commit(repo)
518 result = runner.invoke(
519 cli, ["annotate", "--remove-label", "nosuchlabel", c.commit_id],
520 catch_exceptions=False,
521 )
522 assert result.exit_code == 0
523
524
525 class TestStatusFlag:
526 def test_set_approved(self, repo: pathlib.Path) -> None:
527 c = _write_commit(repo)
528 result = runner.invoke(
529 cli, ["annotate", "--status", "approved", c.commit_id],
530 catch_exceptions=False,
531 )
532 assert result.exit_code == 0
533 assert "approved" in result.output
534
535 def test_set_all_valid_statuses(self, repo: pathlib.Path) -> None:
536 c = _write_commit(repo)
537 for status in ("pending", "approved", "rejected", "needs-review", "wip"):
538 result = runner.invoke(
539 cli, ["annotate", "--status", status, c.commit_id],
540 catch_exceptions=False,
541 )
542 assert result.exit_code == 0
543
544 def test_invalid_status_exits_1(self, repo: pathlib.Path) -> None:
545 c = _write_commit(repo)
546 result = runner.invoke(cli, ["annotate", "--status", "flying", c.commit_id])
547 assert result.exit_code != 0
548
549 def test_status_overwrite(self, repo: pathlib.Path) -> None:
550 c = _write_commit(repo)
551 runner.invoke(cli, ["annotate", "--status", "pending", c.commit_id], catch_exceptions=False)
552 runner.invoke(cli, ["annotate", "--status", "approved", c.commit_id], catch_exceptions=False)
553 stored = read_commit(repo, c.commit_id)
554 assert stored is not None
555 assert stored.status == "approved"
556
557
558 class TestNoteFlag:
559 def test_append_note(self, repo: pathlib.Path) -> None:
560 c = _write_commit(repo)
561 result = runner.invoke(
562 cli, ["annotate", "--note", "looks good", c.commit_id],
563 catch_exceptions=False,
564 )
565 assert result.exit_code == 0
566 assert "looks good" in result.output
567
568 def test_multiple_notes_accumulate(self, repo: pathlib.Path) -> None:
569 c = _write_commit(repo)
570 runner.invoke(cli, ["annotate", "--note", "first", c.commit_id], catch_exceptions=False)
571 runner.invoke(cli, ["annotate", "--note", "second", c.commit_id], catch_exceptions=False)
572 stored = read_commit(repo, c.commit_id)
573 assert stored is not None
574 assert "first" in stored.notes
575 assert "second" in stored.notes
576 assert len(stored.notes) == 2
577
578 def test_empty_note_exits_1(self, repo: pathlib.Path) -> None:
579 c = _write_commit(repo)
580 result = runner.invoke(cli, ["annotate", "--note", " ", c.commit_id])
581 assert result.exit_code != 0
582
583
584 class TestScoreFlag:
585 def test_set_score(self, repo: pathlib.Path) -> None:
586 c = _write_commit(repo)
587 result = runner.invoke(
588 cli, ["annotate", "--score", "0.95", c.commit_id],
589 catch_exceptions=False,
590 )
591 assert result.exit_code == 0
592 assert "0.9500" in result.output
593
594 def test_score_overwrite(self, repo: pathlib.Path) -> None:
595 c = _write_commit(repo)
596 runner.invoke(cli, ["annotate", "--score", "0.5", c.commit_id], catch_exceptions=False)
597 runner.invoke(cli, ["annotate", "--score", "0.9", c.commit_id], catch_exceptions=False)
598 stored = read_commit(repo, c.commit_id)
599 assert stored is not None
600 assert stored.score == pytest.approx(0.9)
601
602 def test_invalid_score_exits_1(self, repo: pathlib.Path) -> None:
603 c = _write_commit(repo)
604 result = runner.invoke(cli, ["annotate", "--score", "2.0", c.commit_id])
605 assert result.exit_code != 0
606
607 def test_score_zero_boundary(self, repo: pathlib.Path) -> None:
608 c = _write_commit(repo)
609 result = runner.invoke(
610 cli, ["annotate", "--score", "0.0", c.commit_id], catch_exceptions=False
611 )
612 assert result.exit_code == 0
613
614 def test_score_one_boundary(self, repo: pathlib.Path) -> None:
615 c = _write_commit(repo)
616 result = runner.invoke(
617 cli, ["annotate", "--score", "1.0", c.commit_id], catch_exceptions=False
618 )
619 assert result.exit_code == 0
620
621
622 class TestCommitResolution:
623 def test_full_commit_id(self, repo: pathlib.Path) -> None:
624 c = _write_commit(repo)
625 result = runner.invoke(cli, ["annotate", c.commit_id], catch_exceptions=False)
626 assert result.exit_code == 0
627
628 def test_short_prefix(self, repo: pathlib.Path) -> None:
629 c = _write_commit(repo)
630 # commit_id may have sha256: prefix — use first 8 hex chars after stripping
631 short = c.commit_id[len("sha256:"):len("sha256:") + 8]
632 result = runner.invoke(cli, ["annotate", short], catch_exceptions=False)
633 assert result.exit_code == 0
634
635 def test_unknown_commit_exits_error(self, repo: pathlib.Path) -> None:
636 (heads_dir(repo) / "main").write_text("nosuchcommit")
637 result = runner.invoke(cli, ["annotate", "nosuchcommit"])
638 assert result.exit_code != 0
639
640 def test_head_implicit(self, repo: pathlib.Path) -> None:
641 _write_commit(repo)
642 result = runner.invoke(cli, ["annotate"], catch_exceptions=False)
643 assert result.exit_code == 0
644
645
646 # ===========================================================================
647 # 4. Security tests
648 # ===========================================================================
649
650
651 class TestSecurity:
652 def test_control_char_in_reviewer_rejected(self, repo: pathlib.Path) -> None:
653 c = _write_commit(repo)
654 result = runner.invoke(cli, ["annotate", "--reviewed-by", "alice\x00", c.commit_id])
655 assert result.exit_code != 0
656
657 def test_ansi_escape_in_reviewer_rejected(self, repo: pathlib.Path) -> None:
658 c = _write_commit(repo)
659 result = runner.invoke(cli, ["annotate", "--reviewed-by", "x\x1b[31my", c.commit_id])
660 assert result.exit_code != 0
661
662 def test_control_char_in_label_rejected(self, repo: pathlib.Path) -> None:
663 c = _write_commit(repo)
664 result = runner.invoke(cli, ["annotate", "--label", "hot\x01fix", c.commit_id])
665 assert result.exit_code != 0
666
667 def test_oversized_reviewer_rejected(self, repo: pathlib.Path) -> None:
668 c = _write_commit(repo)
669 big = "a" * (_MAX_REVIEWER_LEN + 1)
670 result = runner.invoke(cli, ["annotate", "--reviewed-by", big, c.commit_id])
671 assert result.exit_code != 0
672
673 def test_oversized_label_rejected(self, repo: pathlib.Path) -> None:
674 c = _write_commit(repo)
675 big = "x" * (_MAX_LABEL_LEN + 1)
676 result = runner.invoke(cli, ["annotate", "--label", big, c.commit_id])
677 assert result.exit_code != 0
678
679 def test_oversized_note_rejected(self, repo: pathlib.Path) -> None:
680 c = _write_commit(repo)
681 big = "z" * (_MAX_NOTE_LEN + 1)
682 result = runner.invoke(cli, ["annotate", "--note", big, c.commit_id])
683 assert result.exit_code != 0
684
685 def test_invalid_status_value_rejected(self, repo: pathlib.Path) -> None:
686 c = _write_commit(repo)
687 result = runner.invoke(cli, ["annotate", "--status", "APPROVED", c.commit_id])
688 assert result.exit_code != 0
689
690 def test_score_out_of_range_rejected(self, repo: pathlib.Path) -> None:
691 c = _write_commit(repo)
692 result = runner.invoke(cli, ["annotate", "--score", "-1", c.commit_id])
693 assert result.exit_code != 0
694
695 def test_score_nan_rejected(self, repo: pathlib.Path) -> None:
696 c = _write_commit(repo)
697 result = runner.invoke(cli, ["annotate", "--score", "nan", c.commit_id])
698 assert result.exit_code != 0
699
700 def test_error_goes_to_stderr_not_stdout(self, repo: pathlib.Path) -> None:
701 c = _write_commit(repo)
702 result = runner.invoke(cli, ["annotate", "--reviewed-by", "a\x00b", c.commit_id])
703 assert result.exit_code != 0
704 # error detail appears on stderr; stdout carries no diagnostic text
705 assert "❌" in result.stderr
706
707 def test_commit_ref_glob_metachar_safe(self, repo: pathlib.Path) -> None:
708 """A glob metacharacter in the commit ref must not escape path scanning."""
709 _write_commit(repo)
710 result = runner.invoke(cli, ["annotate", "../../../etc/passwd"])
711 assert result.exit_code != 0
712
713
714 # ===========================================================================
715 # 5. Stress tests
716 # ===========================================================================
717
718
719 class TestStress:
720 def test_100_sequential_reviewer_adds(self, repo: pathlib.Path) -> None:
721 c = _write_commit(repo)
722 for i in range(100):
723 runner.invoke(
724 cli, ["annotate", "--reviewed-by", f"agent-{i:03d}", c.commit_id],
725 catch_exceptions=False,
726 )
727 stored = read_commit(repo, c.commit_id)
728 assert stored is not None
729 assert len(stored.reviewed_by) == 100
730
731 def test_50_sequential_test_runs(self, repo: pathlib.Path) -> None:
732 c = _write_commit(repo)
733 for _ in range(50):
734 runner.invoke(cli, ["annotate", "--test-run", c.commit_id], catch_exceptions=False)
735 stored = read_commit(repo, c.commit_id)
736 assert stored is not None
737 assert stored.test_runs == 50
738
739 def test_200_notes_appended(self, repo: pathlib.Path) -> None:
740 c = _write_commit(repo)
741 for i in range(200):
742 runner.invoke(
743 cli, ["annotate", "--note", f"note {i}", c.commit_id],
744 catch_exceptions=False,
745 )
746 stored = read_commit(repo, c.commit_id)
747 assert stored is not None
748 assert len(stored.notes) == 200
749
750 def test_note_at_max_len_accepted(self, repo: pathlib.Path) -> None:
751 c = _write_commit(repo)
752 big_note = "a" * _MAX_NOTE_LEN
753 result = runner.invoke(
754 cli, ["annotate", "--note", big_note, c.commit_id],
755 catch_exceptions=False,
756 )
757 assert result.exit_code == 0
758
759 def test_reviewer_at_max_len_accepted(self, repo: pathlib.Path) -> None:
760 c = _write_commit(repo)
761 big_name = "a" * _MAX_REVIEWER_LEN
762 result = runner.invoke(
763 cli, ["annotate", "--reviewed-by", big_name, c.commit_id],
764 catch_exceptions=False,
765 )
766 assert result.exit_code == 0
767
768 def test_20_labels_added(self, repo: pathlib.Path) -> None:
769 c = _write_commit(repo)
770 for i in range(20):
771 runner.invoke(
772 cli, ["annotate", "--label", f"label-{i}", c.commit_id],
773 catch_exceptions=False,
774 )
775 stored = read_commit(repo, c.commit_id)
776 assert stored is not None
777 assert len(stored.labels) == 20
778
779 def test_status_updated_many_times(self, repo: pathlib.Path) -> None:
780 c = _write_commit(repo)
781 statuses = ["pending", "wip", "needs-review", "approved", "rejected", "approved"]
782 for s in statuses:
783 runner.invoke(cli, ["annotate", "--status", s, c.commit_id], catch_exceptions=False)
784 stored = read_commit(repo, c.commit_id)
785 assert stored is not None
786 assert stored.status == "approved"
787
788
789 # ===========================================================================
790 # 6. Performance tests
791 # ===========================================================================
792
793
794 class TestPerformance:
795 def test_show_annotation_under_200ms(self, repo: pathlib.Path) -> None:
796 c = _write_commit(repo)
797 start = time.monotonic()
798 runner.invoke(cli, ["annotate", c.commit_id], catch_exceptions=False)
799 elapsed = time.monotonic() - start
800 assert elapsed < 0.2, f"show took {elapsed:.3f}s — too slow"
801
802 def test_single_mutation_under_200ms(self, repo: pathlib.Path) -> None:
803 c = _write_commit(repo)
804 start = time.monotonic()
805 runner.invoke(
806 cli, ["annotate", "--reviewed-by", "perf-agent", c.commit_id],
807 catch_exceptions=False,
808 )
809 elapsed = time.monotonic() - start
810 assert elapsed < 0.2, f"mutation took {elapsed:.3f}s — too slow"
811
812 def test_json_output_under_200ms(self, repo: pathlib.Path) -> None:
813 c = _write_commit(repo)
814 start = time.monotonic()
815 runner.invoke(cli, ["annotate", "--json", c.commit_id], catch_exceptions=False)
816 elapsed = time.monotonic() - start
817 assert elapsed < 0.2, f"json output took {elapsed:.3f}s — too slow"
818
819 def test_combined_mutation_under_300ms(self, repo: pathlib.Path) -> None:
820 c = _write_commit(repo)
821 start = time.monotonic()
822 runner.invoke(
823 cli,
824 [
825 "annotate",
826 "--reviewed-by", "alice",
827 "--test-run",
828 "--label", "hotfix",
829 "--status", "pending",
830 "--note", "perf test",
831 "--score", "0.8",
832 c.commit_id,
833 ],
834 catch_exceptions=False,
835 )
836 elapsed = time.monotonic() - start
837 assert elapsed < 0.3, f"combined mutation took {elapsed:.3f}s — too slow"
838
839
840 # ===========================================================================
841 # 7. Data Integrity tests — CRDT semantics
842 # ===========================================================================
843
844
845 class TestDataIntegrity:
846 # ORSet: reviewed_by
847 def test_orset_reviewer_idempotent(self, repo: pathlib.Path) -> None:
848 c = _write_commit(repo)
849 runner.invoke(cli, ["annotate", "--reviewed-by", "alice", c.commit_id], catch_exceptions=False)
850 runner.invoke(cli, ["annotate", "--reviewed-by", "alice", c.commit_id], catch_exceptions=False)
851 stored = read_commit(repo, c.commit_id)
852 assert stored is not None
853 assert stored.reviewed_by.count("alice") == 1
854
855 def test_orset_reviewer_union(self, repo: pathlib.Path) -> None:
856 c = _write_commit(repo)
857 runner.invoke(cli, ["annotate", "--reviewed-by", "alice", c.commit_id], catch_exceptions=False)
858 runner.invoke(cli, ["annotate", "--reviewed-by", "bob", c.commit_id], catch_exceptions=False)
859 stored = read_commit(repo, c.commit_id)
860 assert stored is not None
861 assert "alice" in stored.reviewed_by
862 assert "bob" in stored.reviewed_by
863
864 # ORSet: labels
865 def test_orset_label_idempotent(self, repo: pathlib.Path) -> None:
866 c = _write_commit(repo)
867 runner.invoke(cli, ["annotate", "--label", "hotfix", c.commit_id], catch_exceptions=False)
868 runner.invoke(cli, ["annotate", "--label", "hotfix", c.commit_id], catch_exceptions=False)
869 stored = read_commit(repo, c.commit_id)
870 assert stored is not None
871 assert stored.labels.count("hotfix") == 1
872
873 def test_orset_label_union(self, repo: pathlib.Path) -> None:
874 c = _write_commit(repo)
875 runner.invoke(cli, ["annotate", "--label", "hotfix", c.commit_id], catch_exceptions=False)
876 runner.invoke(cli, ["annotate", "--label", "perf", c.commit_id], catch_exceptions=False)
877 stored = read_commit(repo, c.commit_id)
878 assert stored is not None
879 assert "hotfix" in stored.labels
880 assert "perf" in stored.labels
881
882 # GCounter: test_runs
883 def test_gcounter_monotone(self, repo: pathlib.Path) -> None:
884 c = _write_commit(repo)
885 for expected in range(1, 6):
886 runner.invoke(cli, ["annotate", "--test-run", c.commit_id], catch_exceptions=False)
887 stored = read_commit(repo, c.commit_id)
888 assert stored is not None
889 assert stored.test_runs == expected
890
891 def test_gcounter_never_decrements(self, repo: pathlib.Path) -> None:
892 c = _write_commit(repo)
893 runner.invoke(cli, ["annotate", "--test-run", c.commit_id], catch_exceptions=False)
894 runner.invoke(cli, ["annotate", "--test-run", c.commit_id], catch_exceptions=False)
895 stored = read_commit(repo, c.commit_id)
896 assert stored is not None
897 assert stored.test_runs >= 2
898
899 # LWW: status
900 def test_lww_status_last_write_wins(self, repo: pathlib.Path) -> None:
901 c = _write_commit(repo)
902 runner.invoke(cli, ["annotate", "--status", "pending", c.commit_id], catch_exceptions=False)
903 runner.invoke(cli, ["annotate", "--status", "rejected", c.commit_id], catch_exceptions=False)
904 runner.invoke(cli, ["annotate", "--status", "approved", c.commit_id], catch_exceptions=False)
905 stored = read_commit(repo, c.commit_id)
906 assert stored is not None
907 assert stored.status == "approved"
908
909 # LWW: score
910 def test_lww_score_last_write_wins(self, repo: pathlib.Path) -> None:
911 c = _write_commit(repo)
912 runner.invoke(cli, ["annotate", "--score", "0.3", c.commit_id], catch_exceptions=False)
913 runner.invoke(cli, ["annotate", "--score", "0.7", c.commit_id], catch_exceptions=False)
914 runner.invoke(cli, ["annotate", "--score", "0.1", c.commit_id], catch_exceptions=False)
915 stored = read_commit(repo, c.commit_id)
916 assert stored is not None
917 assert stored.score == pytest.approx(0.1)
918
919 # Append-only: notes
920 def test_notes_append_only_preserves_order(self, repo: pathlib.Path) -> None:
921 c = _write_commit(repo)
922 notes = ["alpha", "beta", "gamma"]
923 for note in notes:
924 runner.invoke(cli, ["annotate", "--note", note, c.commit_id], catch_exceptions=False)
925 stored = read_commit(repo, c.commit_id)
926 assert stored is not None
927 assert stored.notes == notes
928
929 def test_notes_allow_duplicates(self, repo: pathlib.Path) -> None:
930 c = _write_commit(repo)
931 runner.invoke(cli, ["annotate", "--note", "dup", c.commit_id], catch_exceptions=False)
932 runner.invoke(cli, ["annotate", "--note", "dup", c.commit_id], catch_exceptions=False)
933 stored = read_commit(repo, c.commit_id)
934 assert stored is not None
935 assert stored.notes.count("dup") == 2
936
937 # Roundtrip fidelity
938 def test_json_roundtrip_reviewed_by(self, repo: pathlib.Path) -> None:
939 c = _write_commit(repo)
940 runner.invoke(cli, ["annotate", "--reviewed-by", "carol", c.commit_id], catch_exceptions=False)
941 result = runner.invoke(cli, ["annotate", "--json", c.commit_id], catch_exceptions=False)
942 data = json.loads(result.output)
943 assert "carol" in data["reviewed_by"]
944
945 def test_json_roundtrip_score(self, repo: pathlib.Path) -> None:
946 c = _write_commit(repo)
947 runner.invoke(cli, ["annotate", "--score", "0.42", c.commit_id], catch_exceptions=False)
948 result = runner.invoke(cli, ["annotate", "--json", c.commit_id], catch_exceptions=False)
949 data = json.loads(result.output)
950 assert data["score"] == pytest.approx(0.42)
951
952 def test_json_roundtrip_labels(self, repo: pathlib.Path) -> None:
953 c = _write_commit(repo)
954 runner.invoke(cli, ["annotate", "--label", "wip-label", c.commit_id], catch_exceptions=False)
955 result = runner.invoke(cli, ["annotate", "--json", c.commit_id], catch_exceptions=False)
956 data = json.loads(result.output)
957 assert "wip-label" in data["labels"]
958
959 def test_json_changed_false_when_no_mutation(self, repo: pathlib.Path) -> None:
960 c = _write_commit(repo)
961 result = runner.invoke(cli, ["annotate", "--json", c.commit_id], catch_exceptions=False)
962 data = json.loads(result.output)
963 assert data["changed"] is False
964
965 def test_no_changes_message_when_idempotent(self, repo: pathlib.Path) -> None:
966 c = _write_commit(repo)
967 runner.invoke(cli, ["annotate", "--reviewed-by", "alice", c.commit_id], catch_exceptions=False)
968 result = runner.invoke(
969 cli, ["annotate", "--reviewed-by", "alice", c.commit_id],
970 catch_exceptions=False,
971 )
972 assert "no changes" in result.output
File History 4 commits
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 22 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 23 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 30 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 30 days ago