gabriel / musehub public
test_search.py python
681 lines 24.6 KB
Raw
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ breaking 156 days ago
1 """Section 17 — Search: 7-layer test suite.
2
3 Covers gaps in the existing 26 search tests:
4
5 Layer 1 Unit:
6 - _tokenize returns lowercase tokens, ignores punctuation
7 - _tokenize empty string → empty set
8 - _overlap_score full match → 1.0
9 - _overlap_score partial match
10 - _overlap_score no match → 0.0
11 - _overlap_score empty query → 0.0
12 - _commit_to_match round-trips all fields, rounds score to 4dp
13 - _DEFAULT_LIMIT == 20
14 - _STOP_WORDS excludes common words
15
16 Layer 2 Integration:
17 - search_by_keyword with matching commit
18 - search_by_keyword no match → empty matches
19 - search_by_keyword threshold filters low-scoring commits
20 - search_by_ask strips stop-words before scoring
21 - search_by_ask no useful tokens → all commits included
22 - search_by_pattern message match preferred over branch match
23 - search_by_pattern case-insensitive
24 - search_by_property returns empty matches (stub)
25 - _fetch_candidates respects since/until filters
26 - _fetch_candidates caps at 5000
27
28 Layer 3 E2E:
29 - GET /api/v1/search?q=foo returns GlobalSearchResult JSON
30 - GET /api/v1/search missing q → 422
31 - GET /{repo_id}/search/commits?q=foo&mode=keyword → SearchResponse
32 - GET /{repo_id}/search/commits invalid mode → 422
33 - GET /{repo_id}/search/commits unknown repo → 404
34 - GET /{repo_id}/search/commits private repo no auth → 401
35
36 Layer 4 Stress:
37 - 200 commits, keyword search returns at most limit=10
38 - 5 concurrent search_by_keyword calls, all succeed
39
40 Layer 5 Data Integrity:
41 - keyword match_source == "message", score in [0,1]
42 - pattern message match_source == "message"
43 - pattern branch match_source == "branch"
44 - ask mode scores commits with matching tokens higher
45
46 Layer 6 Security:
47 - SQL injection pattern in q param handled safely
48 - XSS probe in q echoed as plain text in JSON (not rendered)
49 - Very long query (10k chars) doesn't crash
50 - null bytes in query handled gracefully
51
52 Layer 7 Performance:
53 - 1000x _tokenize calls in <100ms
54 - search_by_keyword over 500 commits completes in <500ms
55 """
56 from __future__ import annotations
57
58 import asyncio
59 import time
60 import uuid
61 from datetime import datetime, timezone, timedelta
62
63 import pytest
64 from httpx import AsyncClient
65 from sqlalchemy.ext.asyncio import AsyncSession
66
67 from musehub.db.musehub_models import MusehubRepo, MusehubCommit
68 from musehub.muse_cli.models import MuseCliCommit, MuseCliSnapshot
69 from musehub.types.json_types import StrDict
70 from musehub.services.musehub_search import (
71 _tokenize,
72 _overlap_score,
73 _commit_to_match,
74 _DEFAULT_LIMIT,
75 _STOP_WORDS,
76 search_by_ask,
77 search_by_keyword,
78 search_by_pattern,
79 search_by_property,
80 )
81
82
83 # ── Shared helpers ────────────────────────────────────────────────────────────
84
85
86 def _uid() -> str:
87 return str(uuid.uuid4()).replace("-", "")
88
89
90 async def _repo(
91 session: AsyncSession,
92 *,
93 visibility: str = "public",
94 name: str | None = None,
95 ) -> str:
96 name = name or f"repo-{_uid()[:8]}"
97 slug = name[:64]
98 r = MusehubRepo(
99 name=name,
100 owner="testuser",
101 slug=slug,
102 visibility=visibility,
103 owner_user_id="test-owner",
104 )
105 session.add(r)
106 await session.flush()
107 return str(r.repo_id)
108
109
110 async def _snap(session: AsyncSession) -> str:
111 snap_id = f"snap-{_uid()[:16]}"
112 session.add(MuseCliSnapshot(snapshot_id=snap_id, manifest={}))
113 await session.flush()
114 return snap_id
115
116
117 async def _commit(
118 session: AsyncSession,
119 repo_id: str,
120 *,
121 message: str = "test commit",
122 branch: str = "main",
123 author: str = "alice",
124 committed_at: datetime | None = None,
125 ) -> MuseCliCommit:
126 snap_id = await _snap(session)
127 c = MuseCliCommit(
128 commit_id=_uid(),
129 repo_id=repo_id,
130 branch=branch,
131 snapshot_id=snap_id,
132 message=message,
133 author=author,
134 committed_at=committed_at or datetime.now(timezone.utc),
135 )
136 session.add(c)
137 await session.flush()
138 return c
139
140
141 async def _hub_commit(
142 session: AsyncSession,
143 repo_id: str,
144 *,
145 message: str = "hub commit",
146 branch: str = "main",
147 ) -> None:
148 c = MusehubCommit(
149 commit_id=_uid(),
150 repo_id=repo_id,
151 branch=branch,
152 parent_ids=[],
153 message=message,
154 author="testuser",
155 timestamp=datetime.now(tz=timezone.utc),
156 )
157 session.add(c)
158 await session.flush()
159
160
161 # ── Layer 1 — Unit ────────────────────────────────────────────────────────────
162
163
164 class TestUnitTokenize:
165 def test_returns_lowercase_tokens(self) -> None:
166 result = _tokenize("Hello World")
167 assert result == {"hello", "world"}
168
169 def test_ignores_punctuation(self) -> None:
170 result = _tokenize("hello, world! foo-bar")
171 assert "hello" in result
172 assert "world" in result
173 assert "foo" in result
174 assert "bar" in result
175
176 def test_empty_string_returns_empty_set(self) -> None:
177 assert _tokenize("") == set()
178
179 def test_alphanumeric_tokens(self) -> None:
180 result = _tokenize("feat123 fix456")
181 assert "feat123" in result
182 assert "fix456" in result
183
184 def test_deduplicates_tokens(self) -> None:
185 result = _tokenize("foo foo foo")
186 assert result == {"foo"}
187
188
189 class TestUnitOverlapScore:
190 def test_full_match_returns_one(self) -> None:
191 score = _overlap_score({"hello", "world"}, "hello world extra")
192 assert score == 1.0
193
194 def test_partial_match(self) -> None:
195 score = _overlap_score({"hello", "world"}, "hello extra")
196 assert score == 0.5
197
198 def test_no_match_returns_zero(self) -> None:
199 score = _overlap_score({"hello"}, "goodbye universe")
200 assert score == 0.0
201
202 def test_empty_query_returns_zero(self) -> None:
203 score = _overlap_score(set(), "anything goes")
204 assert score == 0.0
205
206 def test_single_token_match(self) -> None:
207 score = _overlap_score({"jazz"}, "jazz fusion bassline")
208 assert score == 1.0
209
210
211 class TestUnitCommitToMatch:
212 def test_round_trips_all_fields(self) -> None:
213 from musehub.models.musehub import SearchCommitMatch
214
215 c = MuseCliCommit(
216 commit_id="abc123",
217 repo_id="repo-1",
218 branch="main",
219 snapshot_id="snap-1",
220 message="add harmony voice",
221 author="alice",
222 committed_at=datetime(2025, 1, 1, tzinfo=timezone.utc),
223 )
224 match = _commit_to_match(c, score=0.12345678, match_source="message")
225 assert isinstance(match, SearchCommitMatch)
226 assert match.commit_id == "abc123"
227 assert match.branch == "main"
228 assert match.message == "add harmony voice"
229 assert match.author == "alice"
230 assert match.score == round(0.12345678, 4)
231 assert match.match_source == "message"
232
233 def test_default_score_is_one(self) -> None:
234 c = MuseCliCommit(
235 commit_id="x",
236 repo_id="r",
237 branch="b",
238 snapshot_id="s",
239 message="m",
240 author="a",
241 committed_at=datetime.now(timezone.utc),
242 )
243 match = _commit_to_match(c)
244 assert match.score == 1.0
245
246 def test_score_rounded_to_4dp(self) -> None:
247 c = MuseCliCommit(
248 commit_id="x",
249 repo_id="r",
250 branch="b",
251 snapshot_id="s",
252 message="m",
253 author="a",
254 committed_at=datetime.now(timezone.utc),
255 )
256 match = _commit_to_match(c, score=1 / 3)
257 assert match.score == round(1 / 3, 4)
258
259
260 class TestUnitConstants:
261 def test_default_limit_is_20(self) -> None:
262 assert _DEFAULT_LIMIT == 20
263
264 def test_stop_words_contains_common_words(self) -> None:
265 for word in ("the", "a", "is", "and", "or", "in", "to"):
266 assert word in _STOP_WORDS
267
268 def test_stop_words_does_not_contain_jazz(self) -> None:
269 assert "jazz" not in _STOP_WORDS
270
271 def test_stop_words_is_frozenset(self) -> None:
272 assert isinstance(_STOP_WORDS, frozenset)
273
274
275 # ── Layer 2 — Integration ─────────────────────────────────────────────────────
276
277
278 class TestIntegrationKeyword:
279 async def test_matching_commit_returned(self, db_session: AsyncSession) -> None:
280 repo_id = await _repo(db_session)
281 await _commit(db_session, repo_id, message="add harmony voice to the mix")
282 await db_session.commit()
283
284 result = await search_by_keyword(db_session, repo_id=repo_id, keyword="harmony")
285 assert len(result.matches) == 1
286 assert "harmony" in result.matches[0].message
287
288 async def test_no_match_returns_empty(self, db_session: AsyncSession) -> None:
289 repo_id = await _repo(db_session)
290 await _commit(db_session, repo_id, message="bassline groove")
291 await db_session.commit()
292
293 result = await search_by_keyword(db_session, repo_id=repo_id, keyword="trumpet")
294 assert result.matches == []
295 assert result.mode == "keyword"
296
297 async def test_threshold_filters_low_scores(self, db_session: AsyncSession) -> None:
298 repo_id = await _repo(db_session)
299 # "jazz rhythm" → keyword="jazz rhythm" → tokens={jazz,rhythm}
300 # commit has only "jazz" → overlap = 0.5
301 await _commit(db_session, repo_id, message="jazz improvisation")
302 await db_session.commit()
303
304 # With threshold=0.8, score=0.5 commit should be excluded.
305 result = await search_by_keyword(
306 db_session, repo_id=repo_id, keyword="jazz rhythm", threshold=0.8
307 )
308 assert result.matches == []
309
310 async def test_mode_field_is_keyword(self, db_session: AsyncSession) -> None:
311 repo_id = await _repo(db_session)
312 await db_session.commit()
313 result = await search_by_keyword(db_session, repo_id=repo_id, keyword="anything")
314 assert result.mode == "keyword"
315
316
317 class TestIntegrationAsk:
318 async def test_strips_stop_words_before_scoring(self, db_session: AsyncSession) -> None:
319 repo_id = await _repo(db_session)
320 # "the jazz" → stop-word "the" removed → keyword "jazz" scored
321 await _commit(db_session, repo_id, message="jazz fusion experiment")
322 await _commit(db_session, repo_id, message="rock anthem beats")
323 await db_session.commit()
324
325 result = await search_by_ask(db_session, repo_id=repo_id, question="the jazz")
326 matched_messages = [m.message for m in result.matches]
327 assert any("jazz" in msg for msg in matched_messages)
328 assert result.mode == "ask"
329
330 async def test_all_stop_words_includes_all_commits(self, db_session: AsyncSession) -> None:
331 repo_id = await _repo(db_session)
332 await _commit(db_session, repo_id, message="first commit message")
333 await _commit(db_session, repo_id, message="second commit message")
334 await db_session.commit()
335
336 # Query made entirely of stop-words → no keywords → score 1.0 for all
337 result = await search_by_ask(
338 db_session, repo_id=repo_id, question="the a is and or"
339 )
340 assert len(result.matches) == 2
341
342
343 class TestIntegrationPattern:
344 async def test_message_match_preferred_over_branch_match(
345 self, db_session: AsyncSession
346 ) -> None:
347 repo_id = await _repo(db_session)
348 # Two commits: one with "jazz" in message, one with "jazz" in branch
349 await _commit(db_session, repo_id, message="jazz fusion", branch="main")
350 await _commit(db_session, repo_id, message="unrelated", branch="jazz-experiment")
351 await db_session.commit()
352
353 result = await search_by_pattern(db_session, repo_id=repo_id, pattern="jazz")
354 assert len(result.matches) == 2
355 # Message match must come first.
356 assert result.matches[0].match_source == "message"
357 assert result.matches[1].match_source == "branch"
358
359 async def test_case_insensitive(self, db_session: AsyncSession) -> None:
360 repo_id = await _repo(db_session)
361 await _commit(db_session, repo_id, message="JAZZ FUSION")
362 await db_session.commit()
363
364 result = await search_by_pattern(db_session, repo_id=repo_id, pattern="jazz")
365 assert len(result.matches) == 1
366
367 async def test_mode_field_is_pattern(self, db_session: AsyncSession) -> None:
368 repo_id = await _repo(db_session)
369 await db_session.commit()
370 result = await search_by_pattern(db_session, repo_id=repo_id, pattern="x")
371 assert result.mode == "pattern"
372
373
374 class TestIntegrationProperty:
375 async def test_returns_empty_matches_stub(self, db_session: AsyncSession) -> None:
376 repo_id = await _repo(db_session)
377 await _commit(db_session, repo_id, message="some commit")
378 await db_session.commit()
379
380 result = await search_by_property(
381 db_session, repo_id=repo_id, harmony="Fmin"
382 )
383 # property mode is a stub — always returns empty matches
384 assert result.matches == []
385 assert result.mode == "property"
386
387
388 class TestIntegrationFetchCandidates:
389 async def test_since_filter(self, db_session: AsyncSession) -> None:
390 repo_id = await _repo(db_session)
391 old = datetime(2020, 1, 1, tzinfo=timezone.utc)
392 new = datetime(2025, 1, 1, tzinfo=timezone.utc)
393 await _commit(db_session, repo_id, message="old commit", committed_at=old)
394 await _commit(db_session, repo_id, message="new commit", committed_at=new)
395 await db_session.commit()
396
397 cutoff = datetime(2024, 1, 1, tzinfo=timezone.utc)
398 result = await search_by_keyword(
399 db_session, repo_id=repo_id, keyword="commit", since=cutoff
400 )
401 assert len(result.matches) == 1
402 assert "new" in result.matches[0].message
403
404 async def test_until_filter(self, db_session: AsyncSession) -> None:
405 repo_id = await _repo(db_session)
406 old = datetime(2020, 1, 1, tzinfo=timezone.utc)
407 new = datetime(2025, 1, 1, tzinfo=timezone.utc)
408 await _commit(db_session, repo_id, message="old commit", committed_at=old)
409 await _commit(db_session, repo_id, message="new commit", committed_at=new)
410 await db_session.commit()
411
412 cutoff = datetime(2022, 1, 1, tzinfo=timezone.utc)
413 result = await search_by_keyword(
414 db_session, repo_id=repo_id, keyword="commit", until=cutoff
415 )
416 assert len(result.matches) == 1
417 assert "old" in result.matches[0].message
418
419
420 # ── Layer 3 — E2E ────────────────────────────────────────────────────────────
421
422
423 class TestE2EApiSearch:
424 async def test_api_search_returns_global_search_result(
425 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
426 ) -> None:
427 repo_id = await _repo(db_session)
428 await _hub_commit(db_session, repo_id, message="melody jazz bassline")
429 await db_session.commit()
430
431 resp = await client.get("/api/search?q=jazz", headers=auth_headers)
432 assert resp.status_code == 200
433 body = resp.json()
434 assert "groups" in body
435
436 async def test_api_search_missing_q_returns_422(
437 self, client: AsyncClient, auth_headers: StrDict
438 ) -> None:
439 resp = await client.get("/api/search", headers=auth_headers)
440 assert resp.status_code == 422
441
442 async def test_musehub_search_keyword_mode(
443 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
444 ) -> None:
445 repo_id = await _repo(db_session, visibility="private")
446 await _commit(db_session, repo_id, message="jazz harmony voice")
447 await db_session.commit()
448
449 resp = await client.get(
450 f"/api/repos/{repo_id}/search?q=jazz&mode=keyword",
451 headers=auth_headers,
452 )
453 assert resp.status_code == 200
454 body = resp.json()
455 assert body["mode"] == "keyword"
456 assert len(body["matches"]) >= 1
457
458 async def test_musehub_search_invalid_mode_returns_422(
459 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
460 ) -> None:
461 repo_id = await _repo(db_session)
462 await db_session.commit()
463
464 resp = await client.get(
465 f"/api/repos/{repo_id}/search?q=foo&mode=badmode",
466 headers=auth_headers,
467 )
468 assert resp.status_code == 422
469
470 async def test_musehub_search_unknown_repo_returns_404(
471 self, client: AsyncClient, auth_headers: StrDict
472 ) -> None:
473 fake_id = str(uuid.uuid4())
474 resp = await client.get(
475 f"/api/repos/{fake_id}/search?q=jazz&mode=keyword",
476 headers=auth_headers,
477 )
478 assert resp.status_code == 404
479
480 async def test_musehub_search_private_repo_no_auth_returns_401(
481 self, client: AsyncClient, db_session: AsyncSession
482 ) -> None:
483 repo_id = await _repo(db_session, visibility="private")
484 await db_session.commit()
485
486 resp = await client.get(
487 f"/api/repos/{repo_id}/search?q=jazz&mode=keyword",
488 )
489 assert resp.status_code == 401
490
491
492 # ── Layer 4 — Stress ─────────────────────────────────────────────────────────
493
494
495 class TestStressSearch:
496 async def test_200_commits_keyword_respects_limit(
497 self, db_session: AsyncSession
498 ) -> None:
499 repo_id = await _repo(db_session)
500 for i in range(200):
501 await _commit(db_session, repo_id, message=f"jazz groove {i}")
502 await db_session.commit()
503
504 result = await search_by_keyword(
505 db_session, repo_id=repo_id, keyword="jazz", limit=10
506 )
507 assert len(result.matches) <= 10
508 assert result.total_scanned >= 200
509
510 async def test_5_concurrent_keyword_searches(
511 self, db_session: AsyncSession
512 ) -> None:
513 repo_id = await _repo(db_session)
514 for i in range(20):
515 await _commit(db_session, repo_id, message=f"harmony beat {i}")
516 await db_session.commit()
517
518 results = [
519 await search_by_keyword(db_session, repo_id=repo_id, keyword="harmony")
520 for _ in range(5)
521 ]
522 assert all(len(r.matches) > 0 for r in results)
523
524
525 # ── Layer 5 — Data Integrity ──────────────────────────────────────────────────
526
527
528 class TestDataIntegritySearch:
529 async def test_keyword_match_source_is_message(
530 self, db_session: AsyncSession
531 ) -> None:
532 repo_id = await _repo(db_session)
533 await _commit(db_session, repo_id, message="jazz fusion experiment")
534 await db_session.commit()
535
536 result = await search_by_keyword(db_session, repo_id=repo_id, keyword="jazz")
537 assert all(m.match_source == "message" for m in result.matches)
538
539 async def test_keyword_score_in_zero_to_one(
540 self, db_session: AsyncSession
541 ) -> None:
542 repo_id = await _repo(db_session)
543 await _commit(db_session, repo_id, message="jazz harmony groove")
544 await db_session.commit()
545
546 result = await search_by_keyword(db_session, repo_id=repo_id, keyword="jazz rhythm harmony")
547 for m in result.matches:
548 assert 0.0 <= m.score <= 1.0
549
550 async def test_pattern_message_match_source(self, db_session: AsyncSession) -> None:
551 repo_id = await _repo(db_session)
552 await _commit(db_session, repo_id, message="jazz fusion")
553 await db_session.commit()
554
555 result = await search_by_pattern(db_session, repo_id=repo_id, pattern="jazz")
556 assert result.matches[0].match_source == "message"
557
558 async def test_pattern_branch_match_source(self, db_session: AsyncSession) -> None:
559 repo_id = await _repo(db_session)
560 await _commit(
561 db_session, repo_id, message="unrelated commit", branch="jazz-experiment"
562 )
563 await db_session.commit()
564
565 result = await search_by_pattern(db_session, repo_id=repo_id, pattern="jazz")
566 assert result.matches[0].match_source == "branch"
567
568 async def test_ask_higher_overlap_scores_higher(
569 self, db_session: AsyncSession
570 ) -> None:
571 repo_id = await _repo(db_session)
572 # high-match commit has both "jazz" and "harmony"
573 await _commit(db_session, repo_id, message="jazz harmony fusion")
574 # low-match commit has only "jazz"
575 await _commit(db_session, repo_id, message="jazz rock experiment")
576 await db_session.commit()
577
578 result = await search_by_ask(
579 db_session, repo_id=repo_id, question="jazz harmony"
580 )
581 assert len(result.matches) >= 2
582 # First result should have the higher-scoring commit
583 assert result.matches[0].score >= result.matches[1].score
584
585
586 # ── Layer 6 — Security ────────────────────────────────────────────────────────
587
588
589 class TestSecuritySearch:
590 async def test_sql_injection_in_pattern_handled_safely(
591 self, db_session: AsyncSession
592 ) -> None:
593 repo_id = await _repo(db_session)
594 await _commit(db_session, repo_id, message="innocent commit")
595 await db_session.commit()
596
597 # SQL injection attempt — should return 0 matches, not crash or return all rows.
598 result = await search_by_pattern(
599 db_session,
600 repo_id=repo_id,
601 pattern="'; DROP TABLE musecli_commits; --",
602 )
603 assert result.matches == []
604
605 async def test_xss_in_query_echoed_in_json_not_rendered(
606 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
607 ) -> None:
608 repo_id = await _repo(db_session, visibility="private")
609 await db_session.commit()
610
611 xss = "<script>alert(1)</script>"
612 resp = await client.get(
613 f"/api/repos/{repo_id}/search?q={xss}&mode=keyword",
614 headers=auth_headers,
615 )
616 assert resp.status_code == 200
617 body = resp.json()
618 # The query is echoed back but must be in JSON (string), not HTML.
619 assert body["query"] == xss
620 assert resp.headers["content-type"].startswith("application/json")
621
622 async def test_very_long_query_does_not_crash(
623 self, db_session: AsyncSession
624 ) -> None:
625 repo_id = await _repo(db_session)
626 await db_session.commit()
627
628 long_query = "jazz " * 2000 # 10k chars
629 result = await search_by_keyword(
630 db_session, repo_id=repo_id, keyword=long_query
631 )
632 assert result.matches == []
633
634 async def test_global_search_route_max_length_enforced(
635 self, client: AsyncClient, auth_headers: StrDict
636 ) -> None:
637 # GET /api/search/repos has max_length=500 on q — over that → 422.
638 long_q = "x" * 501
639 resp = await client.get(f"/api/search/repos?q={long_q}", headers=auth_headers)
640 assert resp.status_code == 422
641
642 async def test_null_byte_in_pattern_handled(
643 self, db_session: AsyncSession
644 ) -> None:
645 repo_id = await _repo(db_session)
646 await db_session.commit()
647
648 # Null bytes must not cause a crash.
649 result = await search_by_pattern(
650 db_session, repo_id=repo_id, pattern="foo\x00bar"
651 )
652 assert isinstance(result.matches, list)
653
654
655 # ── Layer 7 — Performance ─────────────────────────────────────────────────────
656
657
658 class TestPerformanceSearch:
659 def test_1000_tokenize_calls_under_100ms(self) -> None:
660 texts = [f"add harmony voice to track {i}" for i in range(1000)]
661 start = time.perf_counter()
662 for t in texts:
663 _tokenize(t)
664 elapsed = time.perf_counter() - start
665 assert elapsed < 0.1, f"1000 _tokenize calls took {elapsed:.3f}s (expected <0.1s)"
666
667 async def test_keyword_search_500_commits_under_500ms(
668 self, db_session: AsyncSession
669 ) -> None:
670 repo_id = await _repo(db_session)
671 for i in range(500):
672 await _commit(db_session, repo_id, message=f"jazz groove rhythm {i}")
673 await db_session.commit()
674
675 start = time.perf_counter()
676 result = await search_by_keyword(
677 db_session, repo_id=repo_id, keyword="jazz"
678 )
679 elapsed = time.perf_counter() - start
680 assert elapsed < 0.5, f"search over 500 commits took {elapsed:.3f}s (expected <0.5s)"
681 assert len(result.matches) > 0
File History 1 commit
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ 156 days ago