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