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