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