gabriel / muse public
test_code_harmony_plugin.py python
661 lines 24.5 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 days ago
1 """Tests for muse/core/plugins/code_harmony.py.
2
3 ``CodePlugin`` — HarmonyPlugin implementation for code-domain conflicts.
4 ``code_fingerprint(source)`` — normalized token-bag fingerprint for a code snippet.
5
6 The fingerprint encodes a sorted space-separated list of normalized tokens:
7 - Python keywords preserved as-is (``def``, ``return``, ``class``, …)
8 - Identifiers → ``ID``
9 - String literals → ``STR``
10 - Number literals → ``NUM``
11 - Operators/punctuation → preserved (``(``, ``)`` , ``:``, ``+``, …)
12 - Comments → stripped
13 - Whitespace/indentation → irrelevant (sorting makes order-independent)
14
15 Similarity is Tanimoto coefficient on the token multisets:
16 sim = |min(A, B)| / |max(A, B)|
17
18 Also covers the relaxed ``--semantic-fingerprint`` CLI validation that allows
19 non-hex64 fingerprints (required for code plugin fingerprints).
20
21 Coverage tiers
22 --------------
23 I Unit — code_fingerprint output shape, normalization rules
24 II Integration — CodePlugin.similarity on real code snippets
25 III End-to-end — harmony store + engine finds code semantic matches
26 IV Stress — 500-line file fingerprint; large similarity batches
27 V Data integrity — symmetry, bounds [0,1], determinism, Protocol conformance
28 VI Security — malformed Python, very large input, null bytes, empty
29 VII Performance — fingerprint <10ms/function; similarity <1ms; engine <100ms
30 """
31 from __future__ import annotations
32
33 import datetime
34 import pathlib
35 import time
36
37 import pytest
38
39 from muse.core._types import fake_id
40 from muse.core.harmony import (
41 AgentProvenance,
42 ConflictPattern,
43 blob_fingerprint,
44 compute_pattern_id,
45 best_resolution,
46 record_pattern,
47 save_resolution,
48 Resolution,
49 ResolutionStrategy,
50 )
51 from muse.core.harmony_engine import EngineConfig, find_similar
52 from muse.core.plugins.code_harmony import CodePlugin, code_fingerprint
53
54
55 # ---------------------------------------------------------------------------
56 # Shared helpers
57 # ---------------------------------------------------------------------------
58
59
60
61 def _utc_now() -> datetime.datetime:
62 return datetime.datetime.now(datetime.timezone.utc)
63
64
65 @pytest.fixture()
66 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
67 (tmp_path / ".muse").mkdir()
68 return tmp_path
69
70
71 def _make_pattern(
72 path: str,
73 semantic_fp: str,
74 ours: str = "ours",
75 theirs: str = "theirs",
76 ) -> ConflictPattern:
77 ours_id = fake_id(ours)
78 theirs_id = fake_id(theirs)
79 blob_fp = blob_fingerprint(ours_id, theirs_id)
80 pid = compute_pattern_id(path, blob_fp, semantic_fp)
81 return ConflictPattern(
82 pattern_id=pid,
83 path=path,
84 domain="code",
85 conflict_type="content",
86 blob_fingerprint=blob_fp,
87 semantic_fingerprint=semantic_fp,
88 ours_id=ours_id,
89 theirs_id=theirs_id,
90 description={},
91 recorded_at=_utc_now(),
92 recorded_by="test",
93 )
94
95
96 def _make_resolution(pattern_id: str, confidence: float = 0.9) -> Resolution:
97 rid = fake_id(f"res-{pattern_id}")
98 return Resolution(
99 resolution_id=rid,
100 pattern_id=pattern_id,
101 strategy=ResolutionStrategy.MANUAL,
102 policy_id=None,
103 outcome_blob=fake_id("outcome"),
104 resolved_by=AgentProvenance.human(),
105 human_verified=False,
106 confidence=confidence,
107 rationale="test resolution",
108 resolved_at=_utc_now(),
109 )
110
111
112 # ===========================================================================
113 # Tier I — Unit: code_fingerprint()
114 # ===========================================================================
115
116
117 class TestCodeFingerprintShape:
118 """I: output is a non-empty string of space-separated tokens."""
119
120 def test_returns_string(self) -> None:
121 assert isinstance(code_fingerprint("def foo(): pass"), str)
122
123 def test_non_empty_for_real_code(self) -> None:
124 assert code_fingerprint("def foo(): pass") != ""
125
126 def test_empty_source_returns_empty(self) -> None:
127 assert code_fingerprint("") == ""
128
129 def test_whitespace_only_returns_empty(self) -> None:
130 assert code_fingerprint(" \n\t ") == ""
131
132 def test_comment_only_returns_empty(self) -> None:
133 assert code_fingerprint("# just a comment") == ""
134
135 def test_tokens_are_space_separated(self) -> None:
136 fp = code_fingerprint("x = 1")
137 assert " " in fp or len(fp.split()) >= 1
138
139
140 class TestCodeFingerprintNormalization:
141 """I: normalization rules for identifiers, literals, keywords."""
142
143 def test_identifiers_become_ID(self) -> None:
144 fp = code_fingerprint("foo = bar")
145 assert "ID" in fp
146 assert "foo" not in fp
147 assert "bar" not in fp
148
149 def test_numbers_become_NUM(self) -> None:
150 fp = code_fingerprint("x = 42")
151 assert "NUM" in fp
152 assert "42" not in fp
153
154 def test_string_literals_become_STR(self) -> None:
155 fp = code_fingerprint('msg = "hello world"')
156 assert "STR" in fp
157 assert "hello" not in fp
158
159 def test_single_quoted_strings_become_STR(self) -> None:
160 fp = code_fingerprint("msg = 'hello'")
161 assert "STR" in fp
162
163 def test_keywords_preserved(self) -> None:
164 fp = code_fingerprint("def foo(): return None")
165 assert "def" in fp
166 assert "return" in fp
167
168 def test_class_keyword_preserved(self) -> None:
169 fp = code_fingerprint("class Foo: pass")
170 assert "class" in fp
171 assert "pass" in fp
172
173 def test_import_keyword_preserved(self) -> None:
174 fp = code_fingerprint("import os")
175 assert "import" in fp
176
177 def test_comments_stripped(self) -> None:
178 fp_with_comment = code_fingerprint("x = 1 # this is x")
179 fp_without = code_fingerprint("x = 1")
180 assert fp_with_comment == fp_without
181
182 def test_operators_preserved(self) -> None:
183 fp = code_fingerprint("x + y")
184 assert "+" in fp
185
186 def test_parens_preserved(self) -> None:
187 fp = code_fingerprint("foo(x)")
188 assert "(" in fp
189 assert ")" in fp
190
191
192 class TestCodeFingerprintDeterminism:
193 """I: same input → same output, always."""
194
195 def test_deterministic_same_call(self) -> None:
196 src = "def compute(a, b):\n return a * b + 1\n"
197 assert code_fingerprint(src) == code_fingerprint(src)
198
199 def test_indentation_irrelevant(self) -> None:
200 src1 = "def foo(x):\n return x\n"
201 src2 = "def foo(x):\n return x\n"
202 assert code_fingerprint(src1) == code_fingerprint(src2)
203
204 def test_extra_blank_lines_irrelevant(self) -> None:
205 src1 = "def foo():\n pass\n"
206 src2 = "\n\ndef foo():\n\n pass\n\n"
207 assert code_fingerprint(src1) == code_fingerprint(src2)
208
209 def test_output_is_sorted(self) -> None:
210 fp = code_fingerprint("def foo(x): return x")
211 tokens = fp.split()
212 assert tokens == sorted(tokens)
213
214
215 # ===========================================================================
216 # Tier II — Integration: CodePlugin.similarity()
217 # ===========================================================================
218
219
220 class TestCodePluginIdentical:
221 """II: identical or structurally equivalent code → high similarity."""
222
223 def test_identical_source_is_1(self) -> None:
224 src = "def foo(x):\n return x + 1\n"
225 fp = code_fingerprint(src)
226 assert CodePlugin().similarity(fp, fp) == 1.0
227
228 def test_same_structure_different_names_is_1(self) -> None:
229 # Different identifiers, same keyword/operator structure
230 fp1 = code_fingerprint("def foo(x): return x + 1")
231 fp2 = code_fingerprint("def bar(y): return y + 2")
232 assert CodePlugin().similarity(fp1, fp2) == 1.0
233
234 def test_same_import_different_module_is_1(self) -> None:
235 fp1 = code_fingerprint("from typing import Optional")
236 fp2 = code_fingerprint("from os import path")
237 assert CodePlugin().similarity(fp1, fp2) == 1.0
238
239 def test_same_assignment_different_names_high(self) -> None:
240 fp1 = code_fingerprint("result = compute_value(a, b)")
241 fp2 = code_fingerprint("output = process_data(x, y)")
242 assert CodePlugin().similarity(fp1, fp2) > 0.8
243
244
245 class TestCodePluginSimilar:
246 """II: structurally related code → intermediate similarity."""
247
248 def test_same_function_extra_parameter(self) -> None:
249 fp1 = code_fingerprint("def foo(x): return x + 1")
250 fp2 = code_fingerprint("def foo(x, y): return x + y")
251 sim = CodePlugin().similarity(fp1, fp2)
252 assert 0.5 < sim < 1.0
253
254 def test_function_vs_method(self) -> None:
255 fp1 = code_fingerprint("def foo(x):\n return x\n")
256 fp2 = code_fingerprint("def foo(self, x):\n return x\n")
257 sim = CodePlugin().similarity(fp1, fp2)
258 assert 0.5 < sim < 1.0
259
260 def test_added_return_type_annotation(self) -> None:
261 fp1 = code_fingerprint("def foo(x):\n return x\n")
262 fp2 = code_fingerprint("def foo(x) -> int:\n return x\n")
263 sim = CodePlugin().similarity(fp1, fp2)
264 assert sim > 0.6
265
266 def test_added_docstring(self) -> None:
267 fp1 = code_fingerprint("def foo(x):\n return x\n")
268 fp2 = code_fingerprint('def foo(x):\n """Return x."""\n return x\n')
269 sim = CodePlugin().similarity(fp1, fp2)
270 assert sim > 0.5
271
272
273 class TestCodePluginDifferent:
274 """II: structurally unrelated code → low similarity."""
275
276 def test_function_vs_class_low(self) -> None:
277 fp1 = code_fingerprint("def foo(x): return x + 1")
278 fp2 = code_fingerprint("class Foo:\n pass\n")
279 assert CodePlugin().similarity(fp1, fp2) < 0.5
280
281 def test_completely_different_low(self) -> None:
282 fp1 = code_fingerprint(
283 "for item in collection:\n process(item)\n"
284 )
285 fp2 = code_fingerprint(
286 "class DatabaseConnection:\n"
287 " def __init__(self, host, port):\n"
288 " self.host = host\n"
289 " self.port = port\n"
290 )
291 assert CodePlugin().similarity(fp1, fp2) < 0.5
292
293 def test_import_vs_class_low(self) -> None:
294 fp1 = code_fingerprint("import os")
295 fp2 = code_fingerprint("class Foo:\n x = 1\n")
296 assert CodePlugin().similarity(fp1, fp2) < 0.5
297
298
299 class TestCodePluginEdgeCases:
300 """II: empty, single-token, and other edge cases."""
301
302 def test_both_empty_returns_1(self) -> None:
303 assert CodePlugin().similarity("", "") == 1.0
304
305 def test_one_empty_returns_0(self) -> None:
306 fp = code_fingerprint("def foo(): pass")
307 assert CodePlugin().similarity(fp, "") == 0.0
308 assert CodePlugin().similarity("", fp) == 0.0
309
310 def test_single_token_identical(self) -> None:
311 assert CodePlugin().similarity("def", "def") == 1.0
312
313 def test_single_token_different(self) -> None:
314 assert CodePlugin().similarity("def", "class") == 0.0
315
316 def test_returns_float(self) -> None:
317 fp = code_fingerprint("x = 1")
318 result = CodePlugin().similarity(fp, fp)
319 assert isinstance(result, float)
320
321
322 # ===========================================================================
323 # Tier III — End-to-end with harmony store
324 # ===========================================================================
325
326
327 class TestEndToEnd:
328 """III: CodePlugin + harmony store + engine find_similar."""
329
330 def test_engine_finds_similar_code_patterns(self, repo: pathlib.Path) -> None:
331 """Two structurally identical code conflicts → engine proposes via Tier 3."""
332 src_a = "def process(item):\n return item.transform()\n"
333 src_b = "def handle(obj):\n return obj.transform()\n"
334 fp_a = code_fingerprint(src_a)
335 fp_b = code_fingerprint(src_b)
336
337 # Both have the same fingerprint (same structure)
338 assert fp_a == fp_b
339
340 pat_a = _make_pattern("service_a.py", fp_a, ours="oa", theirs="ta")
341 pat_b = _make_pattern("service_b.py", fp_b, ours="ob", theirs="tb")
342 record_pattern(repo, pat_a)
343 record_pattern(repo, pat_b)
344
345 # Give pat_a a resolution
346 res = _make_resolution(pat_a.pattern_id, confidence=0.88)
347 save_resolution(repo, res)
348
349 # find_similar for pat_b via CodePlugin should find pat_a
350 proposals = find_similar(repo, pat_b, plugin=CodePlugin(),
351 config=EngineConfig(semantic_threshold=0.70))
352 assert len(proposals) >= 1
353 assert proposals[0].similar_pattern_id == pat_a.pattern_id
354 assert proposals[0].similarity == 1.0
355
356 def test_dissimilar_patterns_not_proposed(self, repo: pathlib.Path) -> None:
357 """Structurally different code → similarity below threshold → no proposal."""
358 fp_a = code_fingerprint("def foo(x): return x + 1")
359 fp_b = code_fingerprint("class DatabaseManager:\n def __init__(self): pass\n")
360
361 pat_a = _make_pattern("utils.py", fp_a, ours="oa", theirs="ta")
362 pat_b = _make_pattern("db.py", fp_b, ours="ob", theirs="tb")
363 record_pattern(repo, pat_a)
364 record_pattern(repo, pat_b)
365
366 res = _make_resolution(pat_a.pattern_id, confidence=0.9)
367 save_resolution(repo, res)
368
369 proposals = find_similar(repo, pat_b, plugin=CodePlugin(),
370 config=EngineConfig(semantic_threshold=0.70))
371 assert proposals == []
372
373 def test_code_plugin_satisfies_harmony_plugin_protocol(self) -> None:
374 from muse.core.harmony_engine import HarmonyPlugin
375 assert isinstance(CodePlugin(), HarmonyPlugin)
376
377 def test_partial_match_above_threshold_proposed(self, repo: pathlib.Path) -> None:
378 """Partial structural match → sim in (0.5, 1.0) → proposed if above threshold."""
379 fp_a = code_fingerprint("def foo(x):\n return x\n")
380 fp_b = code_fingerprint("def foo(x, y):\n return x + y\n")
381
382 sim = CodePlugin().similarity(fp_a, fp_b)
383 assert 0.5 < sim < 1.0
384
385 pat_a = _make_pattern("a.py", fp_a, ours="oa", theirs="ta")
386 pat_b = _make_pattern("b.py", fp_b, ours="ob", theirs="tb")
387 record_pattern(repo, pat_a)
388 record_pattern(repo, pat_b)
389
390 res = _make_resolution(pat_a.pattern_id)
391 save_resolution(repo, res)
392
393 # Use a low threshold so partial matches are included
394 proposals = find_similar(repo, pat_b, plugin=CodePlugin(),
395 config=EngineConfig(semantic_threshold=0.50))
396 assert len(proposals) >= 1
397
398
399 # ===========================================================================
400 # Tier IV — Stress
401 # ===========================================================================
402
403
404 class TestStress:
405 """IV: large inputs; many-pattern similarity search."""
406
407 def test_fingerprint_500_line_file(self) -> None:
408 lines = []
409 for i in range(50):
410 lines.append(f"def function_{i}(arg_{i}):")
411 lines.append(f" result = arg_{i} * {i}")
412 lines.append(f" return result")
413 lines.append("")
414 src = "\n".join(lines)
415 fp = code_fingerprint(src)
416 assert isinstance(fp, str)
417 assert len(fp) > 0
418
419 def test_similarity_of_large_fingerprints(self) -> None:
420 src = "\n".join(
421 f"def f{i}(x{i}): return x{i} + {i}" for i in range(200)
422 )
423 fp1 = code_fingerprint(src)
424 fp2 = code_fingerprint(src.replace("return", "yield"))
425 sim = CodePlugin().similarity(fp1, fp2)
426 assert 0.0 <= sim <= 1.0
427
428 def test_find_similar_50_patterns(self, repo: pathlib.Path) -> None:
429 target_fp = code_fingerprint("def process(x): return x.run()")
430 target = _make_pattern("target.py", target_fp, ours="to", theirs="tt")
431 record_pattern(repo, target)
432
433 for i in range(50):
434 fp = code_fingerprint(f"def handle_{i}(obj_{i}): return obj_{i}.run()")
435 pat = _make_pattern(f"s{i}.py", fp, ours=f"o{i}", theirs=f"t{i}")
436 record_pattern(repo, pat)
437 save_resolution(repo, _make_resolution(pat.pattern_id))
438
439 proposals = find_similar(repo, target, plugin=CodePlugin(),
440 config=EngineConfig(semantic_threshold=0.70,
441 max_proposals=5))
442 assert len(proposals) <= 5
443 assert len(proposals) >= 1
444
445
446 # ===========================================================================
447 # Tier V — Data integrity
448 # ===========================================================================
449
450
451 class TestDataIntegrity:
452 """V: symmetry, bounds, determinism, Protocol conformance."""
453
454 def test_similarity_symmetric(self) -> None:
455 fp1 = code_fingerprint("def foo(x): return x + 1")
456 fp2 = code_fingerprint("class Bar:\n def method(self): pass\n")
457 plugin = CodePlugin()
458 assert plugin.similarity(fp1, fp2) == plugin.similarity(fp2, fp1)
459
460 def test_similarity_always_in_01(self) -> None:
461 cases = [
462 ("def foo(): pass", "def bar(): pass"),
463 ("x = 1", "y = 'hello'"),
464 ("import os", "class Foo: pass"),
465 ("", ""),
466 ("", "x = 1"),
467 ]
468 plugin = CodePlugin()
469 for a, b in cases:
470 sim = plugin.similarity(code_fingerprint(a), code_fingerprint(b))
471 assert 0.0 <= sim <= 1.0, f"out of range: {sim} for {a!r}, {b!r}"
472
473 def test_fingerprint_deterministic_across_calls(self) -> None:
474 src = "def compute(a, b, c):\n return (a + b) * c\n"
475 fps = [code_fingerprint(src) for _ in range(10)]
476 assert len(set(fps)) == 1
477
478 def test_self_similarity_is_1(self) -> None:
479 for src in [
480 "x = 1",
481 "def foo(x): return x",
482 "class Foo:\n pass",
483 ]:
484 fp = code_fingerprint(src)
485 assert CodePlugin().similarity(fp, fp) == 1.0
486
487 def test_protocol_conformance(self) -> None:
488 from muse.core.harmony_engine import HarmonyPlugin
489 plugin = CodePlugin()
490 assert isinstance(plugin, HarmonyPlugin)
491 assert callable(plugin.similarity)
492
493 def test_fingerprint_is_sorted(self) -> None:
494 src = "def foo(x, y):\n return x + y\n"
495 fp = code_fingerprint(src)
496 tokens = fp.split()
497 assert tokens == sorted(tokens)
498
499
500 # ===========================================================================
501 # Tier VI — Security / robustness
502 # ===========================================================================
503
504
505 class TestSecurity:
506 """VI: malformed input, oversized input, edge cases."""
507
508 def test_malformed_python_does_not_raise(self) -> None:
509 # Syntax error → fallback tokenizer
510 result = code_fingerprint("def foo(:\n return")
511 assert isinstance(result, str)
512
513 def test_unclosed_string_does_not_raise(self) -> None:
514 result = code_fingerprint('x = "unclosed string')
515 assert isinstance(result, str)
516
517 def test_binary_looking_text_does_not_raise(self) -> None:
518 # Non-Python that might confuse the tokenizer
519 result = code_fingerprint("SELECT * FROM users WHERE id = 1;")
520 assert isinstance(result, str)
521
522 def test_very_large_input_does_not_oom(self) -> None:
523 # 500 KB of code-ish text
524 big = "x = 1\n" * 80_000
525 result = code_fingerprint(big)
526 assert isinstance(result, str)
527
528 def test_null_bytes_handled(self) -> None:
529 result = code_fingerprint("x = 1\x00y = 2")
530 assert isinstance(result, str)
531
532 def test_unicode_identifiers_handled(self) -> None:
533 # Python 3 supports unicode identifiers
534 result = code_fingerprint("café = 1")
535 assert isinstance(result, str)
536
537 def test_similarity_with_garbage_strings(self) -> None:
538 plugin = CodePlugin()
539 result = plugin.similarity("garbage###", "more%%%garbage")
540 assert 0.0 <= result <= 1.0
541
542
543 # ===========================================================================
544 # Tier VII — Performance
545 # ===========================================================================
546
547
548 class TestPerformance:
549 """VII: fingerprint <10ms per function; similarity <1ms."""
550
551 def test_fingerprint_typical_function_under_10ms(self) -> None:
552 src = "\n".join([
553 "def process_audio_track(track, sample_rate, channels):",
554 " buffer = AudioBuffer(sample_rate, channels)",
555 " for frame in track.frames:",
556 " normalized = frame.normalize()",
557 " filtered = apply_low_pass(normalized, cutoff=8000)",
558 " buffer.append(filtered)",
559 " return buffer.render(format='wav')",
560 ])
561 start = time.monotonic()
562 code_fingerprint(src)
563 elapsed = (time.monotonic() - start) * 1000
564 assert elapsed < 10, f"fingerprint took {elapsed:.1f}ms"
565
566 def test_fingerprint_100_functions_under_100ms(self) -> None:
567 functions = "\n".join(
568 f"def f{i}(x, y):\n return x + y + {i}\n"
569 for i in range(100)
570 )
571 start = time.monotonic()
572 code_fingerprint(functions)
573 elapsed = (time.monotonic() - start) * 1000
574 assert elapsed < 100, f"fingerprint(100 fns) took {elapsed:.1f}ms"
575
576 def test_similarity_under_1ms(self) -> None:
577 fp1 = code_fingerprint("def foo(x): return x + 1")
578 fp2 = code_fingerprint("def bar(y): return y + 2")
579 start = time.monotonic()
580 CodePlugin().similarity(fp1, fp2)
581 elapsed = (time.monotonic() - start) * 1000
582 assert elapsed < 1, f"similarity took {elapsed:.2f}ms"
583
584 def test_find_similar_20_patterns_under_100ms(
585 self, repo: pathlib.Path
586 ) -> None:
587 fp = code_fingerprint("def run(x): return x.execute()")
588 target = _make_pattern("target.py", fp, ours="to", theirs="tt")
589 record_pattern(repo, target)
590
591 for i in range(20):
592 p = _make_pattern(f"s{i}.py",
593 code_fingerprint(f"def go_{i}(obj_{i}): return obj_{i}.execute()"),
594 ours=f"o{i}", theirs=f"t{i}")
595 record_pattern(repo, p)
596 save_resolution(repo, _make_resolution(p.pattern_id))
597
598 start = time.monotonic()
599 find_similar(repo, target, plugin=CodePlugin())
600 elapsed = (time.monotonic() - start) * 1000
601 assert elapsed < 100, f"find_similar(20) took {elapsed:.1f}ms"
602
603
604 # ===========================================================================
605 # CLI validation: --semantic-fingerprint accepts non-hex64 fingerprints
606 # ===========================================================================
607
608
609 class TestCliFingerprint:
610 """Verify _validate_fingerprint is used (not _validate_id) for semantic_fingerprint."""
611
612 def test_validate_fingerprint_accepts_token_string(self) -> None:
613 from muse.core.harmony import _validate_fingerprint
614 # Should not raise for a normalized token string
615 _validate_fingerprint("( ) + : ID ID ID NUM def return", "semantic_fingerprint")
616
617 def test_validate_fingerprint_accepts_hex64(self) -> None:
618 from muse.core.harmony import _validate_fingerprint
619 _validate_fingerprint(fake_id("anything"), "semantic_fingerprint")
620
621 def test_validate_fingerprint_rejects_empty(self) -> None:
622 from muse.core.harmony import _validate_fingerprint
623 with pytest.raises(ValueError):
624 _validate_fingerprint("", "semantic_fingerprint")
625
626 def test_validate_fingerprint_rejects_null_byte(self) -> None:
627 from muse.core.harmony import _validate_fingerprint
628 with pytest.raises(ValueError):
629 _validate_fingerprint("valid\x00null", "semantic_fingerprint")
630
631 def test_validate_fingerprint_rejects_oversized(self) -> None:
632 from muse.core.harmony import _validate_fingerprint
633 with pytest.raises(ValueError):
634 _validate_fingerprint("x " * 3000, "semantic_fingerprint")
635
636 def test_cli_accepts_code_fingerprint_as_semantic(
637 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
638 ) -> None:
639 """muse harmony record --semantic-fingerprint <token-string> works."""
640 from tests.cli_test_helper import CliRunner
641 muse_dir = tmp_path / ".muse"
642 muse_dir.mkdir()
643 (muse_dir / "config.toml").write_text('[repo]\nname="t"\nid="x"\n')
644 monkeypatch.chdir(tmp_path)
645
646 runner = CliRunner()
647 fp = code_fingerprint("def foo(x): return x + 1")
648 r = runner.invoke(None, [
649 "harmony", "record",
650 "--path", "src/foo.py",
651 "--domain", "code",
652 "--conflict-type", "content",
653 "--ours-id", fake_id("ours"),
654 "--theirs-id", fake_id("theirs"),
655 "--semantic-fingerprint", fp,
656 "--json",
657 ])
658 assert r.exit_code == 0, r.output
659 import json
660 data = json.loads(r.output)
661 assert "pattern_id" in data
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 138 days ago