gabriel / muse public
test_harmony_comprehensive.py python
2,420 lines 91.2 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 days ago
1 """Comprehensive TDD suite for muse.core.harmony.
2
3 Every public API surface is exercised: fingerprinting, pattern CRUD,
4 resolution CRUD, best_resolution ranking, GC, audit, policy management,
5 policy matching, escalation lifecycle, auto_apply exact-replay,
6 auto_apply semantic matching via HarmonyPlugin, record_resolutions,
7 MergeState original_conflict_paths, path-traversal guards, and full
8 end-to-end integration flows.
9
10 Organisation
11 ------------
12 Each TestXxx class covers one coherent API surface. Within each class,
13 tests are ordered: happy path → edge cases → error/adversarial cases.
14 """
15 from __future__ import annotations
16
17 import datetime
18 import pathlib
19
20 import pytest
21
22 import muse.core.harmony as h
23 from muse.core.harmony import (
24 AgentProvenance,
25 AuditEventType,
26 ConflictPattern,
27 ConflictType,
28 EscalationRecord,
29 EscalationStatus,
30 Policy,
31 PolicyAction,
32 PolicyCondition,
33 PolicyScope,
34 Resolution,
35 ResolutionStrategy,
36 append_audit,
37 auto_apply,
38 best_resolution,
39 blob_fingerprint,
40 clear_all,
41 compute_escalation_id,
42 compute_pattern_id,
43 compute_resolution_id,
44 compute_semantic_fingerprint,
45 forget_pattern,
46 gc_stale,
47 increment_applied_count,
48 list_audit,
49 list_escalations,
50 list_patterns,
51 list_policies,
52 list_resolutions,
53 load_escalation,
54 load_pattern,
55 load_policy,
56 load_resolution,
57 match_policy,
58 record_escalation,
59 record_pattern,
60 record_resolutions,
61 remove_policy,
62 resolve_escalation,
63 save_policy,
64 save_resolution,
65 )
66 from muse.core.merge_engine import read_merge_state, write_merge_state
67 from muse.core.object_store import write_object
68 from muse.core._types import Manifest, blob_id, long_id
69 from muse.domain import HarmonyPlugin
70
71
72 # ---------------------------------------------------------------------------
73 # Shared helpers
74 # ---------------------------------------------------------------------------
75
76
77 def _hex64(seed: str) -> str:
78 """Return a valid sha256: content-addressed ID derived from seed.
79
80 Used as a cheap deterministic fingerprint in tests — the seed uniquely
81 determines the ID, so tests can express 'same fingerprint' vs 'different
82 fingerprint' without computing actual content hashes.
83 """
84 return blob_id(seed.encode())
85
86
87 def _write_obj(root: pathlib.Path, content: bytes) -> str:
88 oid = blob_id(content)
89 write_object(root, oid, content)
90 return oid
91
92
93 def _now() -> datetime.datetime:
94 return datetime.datetime.now(datetime.timezone.utc)
95
96
97 def _make_pattern(
98 root: pathlib.Path,
99 *,
100 path: str = "config.py",
101 ours_content: bytes = b"a",
102 theirs_content: bytes = b"b",
103 domain: str = "code",
104 ) -> ConflictPattern:
105 ours_id = _write_obj(root, ours_content)
106 theirs_id = _write_obj(root, theirs_content)
107 blob_fp = blob_fingerprint(ours_id, theirs_id)
108 pattern_id = compute_pattern_id(path, blob_fp, blob_fp)
109 return ConflictPattern(
110 pattern_id=pattern_id,
111 path=path,
112 domain=domain,
113 conflict_type=ConflictType.CONTENT,
114 blob_fingerprint=blob_fp,
115 semantic_fingerprint=blob_fp,
116 ours_id=ours_id,
117 theirs_id=theirs_id,
118 description={},
119 recorded_at=_now(),
120 recorded_by="test",
121 )
122
123
124 def _make_resolution(
125 pattern: ConflictPattern,
126 outcome_content: bytes,
127 root: pathlib.Path,
128 *,
129 confidence: float = 1.0,
130 human_verified: bool = True,
131 applied_count: int = 0,
132 strategy: str = ResolutionStrategy.MANUAL,
133 ) -> Resolution:
134 outcome_blob = _write_obj(root, outcome_content)
135 now = _now()
136 rid = compute_resolution_id(
137 pattern.pattern_id, outcome_blob, strategy, AgentProvenance.human(), now
138 )
139 return Resolution(
140 resolution_id=rid,
141 pattern_id=pattern.pattern_id,
142 strategy=strategy,
143 policy_id=None,
144 outcome_blob=outcome_blob,
145 resolved_by=AgentProvenance.human(),
146 human_verified=human_verified,
147 confidence=confidence,
148 rationale="test",
149 resolved_at=now,
150 applied_count=applied_count,
151 )
152
153
154 def _make_policy(
155 policy_id: str = "test-policy",
156 *,
157 action: str = PolicyAction.PREFER_OURS,
158 scope: str = PolicyScope.REPO,
159 conflict_type: str | None = None,
160 domain: str | None = None,
161 path_pattern: str | None = None,
162 confidence: float = 0.9,
163 ) -> Policy:
164 return Policy(
165 policy_id=policy_id,
166 description="test",
167 when=PolicyCondition(
168 conflict_type=conflict_type,
169 domain=domain,
170 path_pattern=path_pattern,
171 ),
172 action=action,
173 confidence=confidence,
174 escalate_to=None,
175 delegate_to=None,
176 scope=scope,
177 created_at=_now(),
178 created_by="test",
179 )
180
181
182 class _FakePlugin:
183 """Minimal MuseDomainPlugin — no HarmonyPlugin sub-protocol."""
184 name = "test"
185 def schema(self): return {}
186
187
188 def _noop(*args, **kwargs):
189 """Stub for unused MuseDomainPlugin methods in test doubles."""
190 return {}
191
192
193 class _SemanticPlugin:
194 """HarmonyPlugin that returns a fixed semantic fingerprint.
195
196 Implements all MuseDomainPlugin required methods so that
197 isinstance(plugin, HarmonyPlugin) returns True at runtime.
198 """
199 name = "semantic"
200
201 def __init__(self, fixed_fp: str):
202 self._fp = fixed_fp
203
204 def schema(self): return {}
205 def snapshot(self, live_state): return _noop()
206 def diff(self, base, target, *, repo_root=None): return _noop()
207 def merge(self, base, left, right, *, repo_root=None): return _noop()
208 def apply(self, delta, live_state): return _noop()
209 def drift(self, *args, **kwargs): return _noop()
210
211 def conflict_fingerprint(
212 self,
213 path: str,
214 ours_id: str,
215 theirs_id: str,
216 repo_root: pathlib.Path,
217 ) -> str:
218 return self._fp
219
220
221 class _ThrowingPlugin:
222 """HarmonyPlugin that always raises during conflict_fingerprint."""
223 name = "throwing"
224
225 def schema(self): return {}
226 def snapshot(self, live_state): return _noop()
227 def diff(self, base, target, *, repo_root=None): return _noop()
228 def merge(self, base, left, right, *, repo_root=None): return _noop()
229 def apply(self, delta, live_state): return _noop()
230 def drift(self, *args, **kwargs): return _noop()
231
232 def conflict_fingerprint(self, path, ours_id, theirs_id, repo_root):
233 raise RuntimeError("deliberate error")
234
235
236 class _BadLengthPlugin:
237 """HarmonyPlugin that returns a fingerprint of the wrong length."""
238 name = "bad"
239
240 def schema(self): return {}
241 def snapshot(self, live_state): return _noop()
242 def diff(self, base, target, *, repo_root=None): return _noop()
243 def merge(self, base, left, right, *, repo_root=None): return _noop()
244 def apply(self, delta, live_state): return _noop()
245 def drift(self, *args, **kwargs): return _noop()
246
247 def conflict_fingerprint(self, path, ours_id, theirs_id, repo_root):
248 return "tooshort"
249
250
251 @pytest.fixture()
252 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
253 (tmp_path / ".muse").mkdir()
254 return tmp_path
255
256
257 # ===========================================================================
258 # 1. blob_fingerprint
259 # ===========================================================================
260
261
262 class TestBlobFingerprint:
263 def test_returns_sha256_id(self) -> None:
264 ours = blob_id(b"x")
265 theirs = blob_id(b"y")
266 fp = blob_fingerprint(ours, theirs)
267 assert fp.startswith("sha256:")
268 assert len(fp) == 71
269
270 def test_commutative(self) -> None:
271 a = blob_id(b"alpha")
272 b = blob_id(b"beta")
273 assert blob_fingerprint(a, b) == blob_fingerprint(b, a)
274
275 def test_stable(self) -> None:
276 a = blob_id(b"stable")
277 b = blob_id(b"input")
278 assert blob_fingerprint(a, b) == blob_fingerprint(a, b)
279
280 def test_different_pairs_produce_different_fingerprints(self) -> None:
281 a, b, c = blob_id(b"a"), blob_id(b"b"), blob_id(b"c")
282 assert blob_fingerprint(a, b) != blob_fingerprint(a, c)
283 assert blob_fingerprint(a, b) != blob_fingerprint(b, c)
284
285 def test_same_id_both_sides(self) -> None:
286 # Degenerate case: ours == theirs (no actual conflict)
287 a = blob_id(b"same")
288 fp = blob_fingerprint(a, a)
289 assert len(fp) == 71 and fp.startswith("sha256:") # still produces valid output
290
291
292 # ===========================================================================
293 # 2. compute_pattern_id
294 # ===========================================================================
295
296
297 class TestComputePatternId:
298 def test_returns_sha256_id(self) -> None:
299 fp = blob_fingerprint(blob_id(b"a"), blob_id(b"b"))
300 pid = compute_pattern_id("f.py", fp, fp)
301 assert pid.startswith("sha256:")
302 assert len(pid) == 71
303
304 def test_stable(self) -> None:
305 assert (
306 compute_pattern_id("f.py", _hex64("a"), _hex64("b"))
307 == compute_pattern_id("f.py", _hex64("a"), _hex64("b"))
308 )
309
310 def test_path_sensitive(self) -> None:
311 blob = _hex64("fp")
312 sem = _hex64("sfp")
313 assert compute_pattern_id("a.py", blob, sem) != compute_pattern_id("b.py", blob, sem)
314
315 def test_blob_fp_sensitive_when_no_semantic_plugin(self) -> None:
316 """When blob_fp == semantic_fp (no plugin), blob_fp determines identity."""
317 # Same blob_fp used for both slots (exact-replay mode)
318 fp1 = _hex64("fp1")
319 fp2 = _hex64("fp2")
320 # When blob_fp == semantic_fp, the formula is f"{path}:{blob_fp}:{semantic_fp}"
321 assert compute_pattern_id("f.py", fp1, fp1) != compute_pattern_id("f.py", fp2, fp2)
322
323 def test_blob_fp_irrelevant_when_semantic_fp_differs(self) -> None:
324 """When semantic_fp ≠ blob_fp, pattern_id depends only on semantic_fp and path.
325
326 This is the semantic plugin model: two conflicts with different blob IDs
327 but the same semantic fingerprint map to the same pattern, enabling
328 cross-content replay.
329 """
330 sem = _hex64("sfp")
331 # Two different blob_fps with the same semantic_fp → same pattern_id
332 pid1 = compute_pattern_id("f.py", _hex64("fp1"), sem)
333 pid2 = compute_pattern_id("f.py", _hex64("fp2"), sem)
334 assert pid1 == pid2, (
335 "When semantic_fp != blob_fp, pattern_id must depend only on "
336 "semantic_fp — different blob pairs with the same semantic shape "
337 "must share a pattern to enable cross-content replay"
338 )
339
340 def test_semantic_fp_sensitive(self) -> None:
341 blob = _hex64("bfp")
342 assert (
343 compute_pattern_id("f.py", blob, _hex64("s1"))
344 != compute_pattern_id("f.py", blob, _hex64("s2"))
345 )
346
347 def test_symbol_path_distinct_from_file_path(self) -> None:
348 blob = _hex64("fp")
349 sem = _hex64("sfp")
350 assert (
351 compute_pattern_id("config.py", blob, sem)
352 != compute_pattern_id("config.py::MAX_CONNECTIONS", blob, sem)
353 )
354
355
356 # ===========================================================================
357 # 3. compute_semantic_fingerprint
358 # ===========================================================================
359
360
361 class TestComputeSemanticFingerprint:
362 def test_no_plugin_returns_blob_fingerprint(self, repo: pathlib.Path) -> None:
363 ours = blob_id(b"x")
364 theirs = blob_id(b"y")
365 result = compute_semantic_fingerprint("f.py", ours, theirs, _FakePlugin(), repo)
366 assert result == blob_fingerprint(ours, theirs)
367
368 def test_commutative_no_plugin(self, repo: pathlib.Path) -> None:
369 ours = blob_id(b"x")
370 theirs = blob_id(b"y")
371 r1 = compute_semantic_fingerprint("f.py", ours, theirs, _FakePlugin(), repo)
372 r2 = compute_semantic_fingerprint("f.py", theirs, ours, _FakePlugin(), repo)
373 assert r1 == r2
374
375 def test_harmony_plugin_used_when_available(self, repo: pathlib.Path) -> None:
376 fixed = _hex64("fixed-semantic")
377 plugin = _SemanticPlugin(fixed)
378 ours = blob_id(b"x")
379 theirs = blob_id(b"y")
380 result = compute_semantic_fingerprint("f.py", ours, theirs, plugin, repo)
381 assert result == fixed
382
383 def test_throwing_plugin_falls_back_to_blob(self, repo: pathlib.Path) -> None:
384 ours = blob_id(b"x")
385 theirs = blob_id(b"y")
386 result = compute_semantic_fingerprint(
387 "f.py", ours, theirs, _ThrowingPlugin(), repo
388 )
389 assert result == blob_fingerprint(ours, theirs)
390
391 def test_bad_length_plugin_falls_back_to_blob(self, repo: pathlib.Path) -> None:
392 ours = blob_id(b"x")
393 theirs = blob_id(b"y")
394 result = compute_semantic_fingerprint(
395 "f.py", ours, theirs, _BadLengthPlugin(), repo
396 )
397 assert result == blob_fingerprint(ours, theirs)
398
399 def test_path_does_not_affect_blob_fallback(self, repo: pathlib.Path) -> None:
400 ours = blob_id(b"x")
401 theirs = blob_id(b"y")
402 r1 = compute_semantic_fingerprint("a.py", ours, theirs, _FakePlugin(), repo)
403 r2 = compute_semantic_fingerprint("b.py", ours, theirs, _FakePlugin(), repo)
404 # blob_fingerprint is path-independent
405 assert r1 == r2
406
407
408 # ===========================================================================
409 # 4. record_pattern / load_pattern
410 # ===========================================================================
411
412
413 class TestRecordPatternAndLoad:
414 def test_record_then_load(self, repo: pathlib.Path) -> None:
415 p = _make_pattern(repo)
416 record_pattern(repo, p)
417 loaded = load_pattern(repo, p.pattern_id)
418 assert loaded is not None
419 assert loaded.pattern_id == p.pattern_id
420 assert loaded.path == p.path
421 assert loaded.domain == p.domain
422
423 def test_record_idempotent(self, repo: pathlib.Path) -> None:
424 p = _make_pattern(repo)
425 record_pattern(repo, p)
426 record_pattern(repo, p) # second call must be a no-op
427 assert len(list_patterns(repo)) == 1
428
429 def test_load_missing_returns_none(self, repo: pathlib.Path) -> None:
430 assert load_pattern(repo, _hex64("missing")) is None
431
432 def test_load_invalid_id_returns_none(self, repo: pathlib.Path) -> None:
433 assert load_pattern(repo, "not-hex-64") is None
434
435 def test_record_invalid_id_raises(self, repo: pathlib.Path) -> None:
436 p = _make_pattern(repo)
437 # Replace the pattern_id with an invalid value via dataclass replace
438 from dataclasses import replace as dc_replace
439 bad_p = dc_replace(p, pattern_id="short")
440 with pytest.raises(ValueError):
441 record_pattern(repo, bad_p)
442
443 def test_all_fields_round_trip(self, repo: pathlib.Path) -> None:
444 p = _make_pattern(repo, path="src/api.py::handle_request", domain="code")
445 record_pattern(repo, p)
446 loaded = load_pattern(repo, p.pattern_id)
447 assert loaded is not None
448 assert loaded.path == "src/api.py::handle_request"
449 assert loaded.conflict_type == ConflictType.CONTENT
450 assert loaded.blob_fingerprint == p.blob_fingerprint
451
452
453 # ===========================================================================
454 # 5. list_patterns
455 # ===========================================================================
456
457
458 class TestListPatterns:
459 def test_empty_store(self, repo: pathlib.Path) -> None:
460 assert list_patterns(repo) == []
461
462 def test_one_pattern(self, repo: pathlib.Path) -> None:
463 p = _make_pattern(repo)
464 record_pattern(repo, p)
465 patterns = list_patterns(repo)
466 assert len(patterns) == 1
467 assert patterns[0].pattern_id == p.pattern_id
468
469 def test_multiple_patterns(self, repo: pathlib.Path) -> None:
470 for i in range(5):
471 p = _make_pattern(repo, ours_content=bytes([i]), theirs_content=bytes([i + 10]))
472 record_pattern(repo, p)
473 assert len(list_patterns(repo)) == 5
474
475 def test_distinct_paths_produce_distinct_patterns(self, repo: pathlib.Path) -> None:
476 ours = _write_obj(repo, b"ours")
477 theirs = _write_obj(repo, b"theirs")
478 paths = ["a.py::foo", "a.py::bar", "b.py::baz"]
479 for path in paths:
480 blob_fp = blob_fingerprint(ours, theirs)
481 pid = compute_pattern_id(path, blob_fp, blob_fp)
482 from dataclasses import replace as dc_replace
483 p = dc_replace(
484 _make_pattern(repo),
485 pattern_id=pid,
486 path=path,
487 ours_id=ours,
488 theirs_id=theirs,
489 blob_fingerprint=blob_fp,
490 semantic_fingerprint=blob_fp,
491 )
492 record_pattern(repo, p)
493 result = list_patterns(repo)
494 assert len(result) == 3
495 stored_paths = {r.path for r in result}
496 assert stored_paths == set(paths)
497
498
499 # ===========================================================================
500 # 6. forget_pattern
501 # ===========================================================================
502
503
504 class TestForgetPattern:
505 def test_forget_existing(self, repo: pathlib.Path) -> None:
506 p = _make_pattern(repo)
507 record_pattern(repo, p)
508 assert forget_pattern(repo, p.pattern_id) is True
509 assert load_pattern(repo, p.pattern_id) is None
510
511 def test_forget_missing_returns_false(self, repo: pathlib.Path) -> None:
512 assert forget_pattern(repo, _hex64("gone")) is False
513
514 def test_forget_also_removes_resolutions(self, repo: pathlib.Path) -> None:
515 p = _make_pattern(repo)
516 record_pattern(repo, p)
517 r = _make_resolution(p, b"outcome", repo)
518 save_resolution(repo, r)
519 forget_pattern(repo, p.pattern_id)
520 assert list_resolutions(repo, p.pattern_id) == []
521
522 def test_invalid_id_returns_false(self, repo: pathlib.Path) -> None:
523 assert forget_pattern(repo, "bad-id") is False
524
525
526 # ===========================================================================
527 # 7. clear_all
528 # ===========================================================================
529
530
531 class TestClearAll:
532 def test_empty_store(self, repo: pathlib.Path) -> None:
533 assert clear_all(repo) == 0
534
535 def test_clears_all_patterns(self, repo: pathlib.Path) -> None:
536 for i in range(3):
537 p = _make_pattern(repo, ours_content=bytes([i]), theirs_content=bytes([i + 20]))
538 record_pattern(repo, p)
539 assert clear_all(repo) == 3
540 assert list_patterns(repo) == []
541
542 def test_clears_patterns_with_resolutions(self, repo: pathlib.Path) -> None:
543 p = _make_pattern(repo)
544 record_pattern(repo, p)
545 r = _make_resolution(p, b"out", repo)
546 save_resolution(repo, r)
547 clear_all(repo)
548 assert list_patterns(repo) == []
549 assert list_resolutions(repo, p.pattern_id) == []
550
551
552 # ===========================================================================
553 # 8. save_resolution / load_resolution
554 # ===========================================================================
555
556
557 class TestSaveAndLoadResolution:
558 def test_save_then_load(self, repo: pathlib.Path) -> None:
559 p = _make_pattern(repo)
560 record_pattern(repo, p)
561 r = _make_resolution(p, b"resolved content", repo)
562 save_resolution(repo, r)
563 loaded = load_resolution(repo, p.pattern_id, r.resolution_id)
564 assert loaded is not None
565 assert loaded.resolution_id == r.resolution_id
566 assert loaded.outcome_blob == r.outcome_blob
567 assert loaded.human_verified is True
568 assert loaded.confidence == 1.0
569
570 def test_load_missing_returns_none(self, repo: pathlib.Path) -> None:
571 p = _make_pattern(repo)
572 record_pattern(repo, p)
573 assert load_resolution(repo, p.pattern_id, _hex64("gone")) is None
574
575 def test_save_is_idempotent(self, repo: pathlib.Path) -> None:
576 """save_resolution is explicitly idempotent — second call is a no-op."""
577 p = _make_pattern(repo)
578 record_pattern(repo, p)
579 r = _make_resolution(p, b"v1", repo)
580 save_resolution(repo, r)
581 # Second call with same resolution_id must not raise and must not overwrite
582 from dataclasses import replace as dc_replace
583 r2 = dc_replace(r, applied_count=99, confidence=0.01)
584 save_resolution(repo, r2) # must be a no-op
585 loaded = load_resolution(repo, p.pattern_id, r.resolution_id)
586 assert loaded is not None
587 assert loaded.applied_count == 0 # original value preserved
588 assert loaded.confidence == 1.0 # original value preserved
589
590 def test_save_requires_existing_pattern(self, repo: pathlib.Path) -> None:
591 """save_resolution raises FileNotFoundError when pattern doesn't exist."""
592 fake_pattern_id = _hex64("nonexistent-pattern")
593 outcome = _write_obj(repo, b"outcome")
594 now = _now()
595 prov = AgentProvenance.human()
596 rid = compute_resolution_id(fake_pattern_id, outcome, ResolutionStrategy.MANUAL, prov, now)
597 r = Resolution(
598 resolution_id=rid,
599 pattern_id=fake_pattern_id,
600 strategy=ResolutionStrategy.MANUAL,
601 policy_id=None,
602 outcome_blob=outcome,
603 resolved_by=prov,
604 human_verified=False,
605 confidence=0.5,
606 rationale="orphan",
607 resolved_at=now,
608 applied_count=0,
609 )
610 with pytest.raises(FileNotFoundError):
611 save_resolution(repo, r)
612
613 def test_agent_provenance_round_trip(self, repo: pathlib.Path) -> None:
614 p = _make_pattern(repo)
615 record_pattern(repo, p)
616 outcome = _write_obj(repo, b"agent-resolved")
617 now = _now()
618 prov = AgentProvenance.agent("claude-code", "claude-sonnet-4-6")
619 rid = compute_resolution_id(
620 p.pattern_id, outcome, ResolutionStrategy.EXACT_REPLAY, prov, now
621 )
622 r = Resolution(
623 resolution_id=rid,
624 pattern_id=p.pattern_id,
625 strategy=ResolutionStrategy.EXACT_REPLAY,
626 policy_id=None,
627 outcome_blob=outcome,
628 resolved_by=prov,
629 human_verified=False,
630 confidence=0.95,
631 rationale="agent resolved",
632 resolved_at=now,
633 applied_count=0,
634 )
635 save_resolution(repo, r)
636 loaded = load_resolution(repo, p.pattern_id, rid)
637 assert loaded is not None
638 assert loaded.resolved_by.type == "agent"
639 assert loaded.resolved_by.agent_id == "claude-code"
640 assert loaded.resolved_by.model_id == "claude-sonnet-4-6"
641 assert loaded.confidence == 0.95
642 assert loaded.human_verified is False
643
644
645 # ===========================================================================
646 # 9. list_resolutions + sorting
647 # ===========================================================================
648
649
650 class TestListResolutions:
651 def test_empty(self, repo: pathlib.Path) -> None:
652 p = _make_pattern(repo)
653 record_pattern(repo, p)
654 assert list_resolutions(repo, p.pattern_id) == []
655
656 def test_single(self, repo: pathlib.Path) -> None:
657 p = _make_pattern(repo)
658 record_pattern(repo, p)
659 r = _make_resolution(p, b"out", repo)
660 save_resolution(repo, r)
661 result = list_resolutions(repo, p.pattern_id)
662 assert len(result) == 1
663 assert result[0].resolution_id == r.resolution_id
664
665 def test_sorted_human_verified_first(self, repo: pathlib.Path) -> None:
666 """human_verified=True must sort before human_verified=False."""
667 p = _make_pattern(repo)
668 record_pattern(repo, p)
669
670 unverified = _make_resolution(p, b"unverified", repo, human_verified=False, confidence=1.0)
671 verified = _make_resolution(p, b"verified", repo, human_verified=True, confidence=0.5)
672 save_resolution(repo, unverified)
673 save_resolution(repo, verified)
674
675 result = list_resolutions(repo, p.pattern_id)
676 assert result[0].human_verified is True
677
678 def test_sorted_confidence_descending(self, repo: pathlib.Path) -> None:
679 p = _make_pattern(repo)
680 record_pattern(repo, p)
681
682 low = _make_resolution(p, b"low", repo, confidence=0.3, human_verified=False)
683 high = _make_resolution(p, b"high", repo, confidence=0.9, human_verified=False)
684 save_resolution(repo, low)
685 save_resolution(repo, high)
686
687 result = list_resolutions(repo, p.pattern_id)
688 assert result[0].confidence == 0.9
689
690 def test_sorted_applied_count_descending_as_tiebreaker(
691 self, repo: pathlib.Path
692 ) -> None:
693 p = _make_pattern(repo)
694 record_pattern(repo, p)
695
696 rarely = _make_resolution(p, b"rarely", repo, confidence=0.8, applied_count=1)
697 often = _make_resolution(p, b"often", repo, confidence=0.8, applied_count=10)
698 save_resolution(repo, rarely)
699 save_resolution(repo, often)
700
701 result = list_resolutions(repo, p.pattern_id)
702 assert result[0].applied_count == 10
703
704 def test_invalid_pattern_id_returns_empty(self, repo: pathlib.Path) -> None:
705 assert list_resolutions(repo, "bad-id") == []
706
707
708 # ===========================================================================
709 # 10. best_resolution
710 # ===========================================================================
711
712
713 class TestBestResolution:
714 def test_returns_none_when_no_resolutions(self, repo: pathlib.Path) -> None:
715 p = _make_pattern(repo)
716 record_pattern(repo, p)
717 assert best_resolution(repo, p.pattern_id) is None
718
719 def test_returns_only_resolution(self, repo: pathlib.Path) -> None:
720 p = _make_pattern(repo)
721 record_pattern(repo, p)
722 r = _make_resolution(p, b"out", repo)
723 save_resolution(repo, r)
724 best = best_resolution(repo, p.pattern_id)
725 assert best is not None
726 assert best.resolution_id == r.resolution_id
727
728 def test_prefers_human_verified_over_high_confidence(
729 self, repo: pathlib.Path
730 ) -> None:
731 p = _make_pattern(repo)
732 record_pattern(repo, p)
733
734 agent_high = _make_resolution(
735 p, b"agent", repo, human_verified=False, confidence=0.99
736 )
737 human_low = _make_resolution(
738 p, b"human", repo, human_verified=True, confidence=0.5
739 )
740 save_resolution(repo, agent_high)
741 save_resolution(repo, human_low)
742
743 best = best_resolution(repo, p.pattern_id)
744 assert best is not None
745 assert best.human_verified is True
746
747 def test_prefers_higher_confidence(self, repo: pathlib.Path) -> None:
748 p = _make_pattern(repo)
749 record_pattern(repo, p)
750
751 low = _make_resolution(p, b"low", repo, confidence=0.4, human_verified=False)
752 high = _make_resolution(p, b"high", repo, confidence=0.8, human_verified=False)
753 save_resolution(repo, low)
754 save_resolution(repo, high)
755
756 best = best_resolution(repo, p.pattern_id)
757 assert best is not None
758 assert best.confidence == 0.8
759
760 def test_prefers_higher_applied_count_as_tiebreaker(
761 self, repo: pathlib.Path
762 ) -> None:
763 p = _make_pattern(repo)
764 record_pattern(repo, p)
765
766 rare = _make_resolution(p, b"rare", repo, applied_count=2, confidence=0.9)
767 freq = _make_resolution(p, b"freq", repo, applied_count=20, confidence=0.9)
768 save_resolution(repo, rare)
769 save_resolution(repo, freq)
770
771 best = best_resolution(repo, p.pattern_id)
772 assert best is not None
773 assert best.applied_count == 20
774
775
776 # ===========================================================================
777 # 11. increment_applied_count
778 # ===========================================================================
779
780
781 class TestIncrementAppliedCount:
782 def test_increments_from_zero(self, repo: pathlib.Path) -> None:
783 p = _make_pattern(repo)
784 record_pattern(repo, p)
785 r = _make_resolution(p, b"out", repo, applied_count=0)
786 save_resolution(repo, r)
787
788 result = increment_applied_count(repo, p.pattern_id, r.resolution_id)
789 assert result is True
790
791 loaded = load_resolution(repo, p.pattern_id, r.resolution_id)
792 assert loaded is not None
793 assert loaded.applied_count == 1
794
795 def test_increments_multiple_times(self, repo: pathlib.Path) -> None:
796 p = _make_pattern(repo)
797 record_pattern(repo, p)
798 r = _make_resolution(p, b"out", repo, applied_count=0)
799 save_resolution(repo, r)
800
801 for _ in range(5):
802 increment_applied_count(repo, p.pattern_id, r.resolution_id)
803
804 loaded = load_resolution(repo, p.pattern_id, r.resolution_id)
805 assert loaded is not None
806 assert loaded.applied_count == 5
807
808 def test_returns_false_for_missing_resolution(self, repo: pathlib.Path) -> None:
809 p = _make_pattern(repo)
810 record_pattern(repo, p)
811 result = increment_applied_count(repo, p.pattern_id, _hex64("gone"))
812 assert result is False
813
814
815 # ===========================================================================
816 # 12. gc_stale
817 # ===========================================================================
818
819
820 class TestGcStale:
821 def test_empty_store(self, repo: pathlib.Path) -> None:
822 assert gc_stale(repo, age_days=0) == 0
823
824 def test_removes_old_pattern_without_resolution(self, repo: pathlib.Path) -> None:
825 p = _make_pattern(repo)
826 from dataclasses import replace as dc_replace
827 old_time = _now() - datetime.timedelta(days=200)
828 old_p = dc_replace(p, recorded_at=old_time)
829 record_pattern(repo, old_p)
830
831 removed = gc_stale(repo, age_days=90)
832 assert removed == 1
833 assert list_patterns(repo) == []
834
835 def test_keeps_pattern_with_resolution(self, repo: pathlib.Path) -> None:
836 p = _make_pattern(repo)
837 from dataclasses import replace as dc_replace
838 old_time = _now() - datetime.timedelta(days=200)
839 old_p = dc_replace(p, recorded_at=old_time)
840 record_pattern(repo, old_p)
841 r = _make_resolution(old_p, b"resolved", repo)
842 save_resolution(repo, r)
843
844 removed = gc_stale(repo, age_days=90)
845 assert removed == 0
846 assert len(list_patterns(repo)) == 1
847
848 def test_keeps_recent_pattern_without_resolution(self, repo: pathlib.Path) -> None:
849 p = _make_pattern(repo)
850 record_pattern(repo, p) # recorded_at = now
851
852 removed = gc_stale(repo, age_days=90)
853 assert removed == 0
854 assert len(list_patterns(repo)) == 1
855
856 def test_removes_only_old_stale_patterns(self, repo: pathlib.Path) -> None:
857 """Mix of old-stale, old-resolved, new-stale — only old-stale removed."""
858 from dataclasses import replace as dc_replace
859
860 old_stale = _make_pattern(repo, ours_content=b"old_s_o", theirs_content=b"old_s_t")
861 old_resolved = _make_pattern(repo, ours_content=b"old_r_o", theirs_content=b"old_r_t")
862 new_stale = _make_pattern(repo, ours_content=b"new_s_o", theirs_content=b"new_s_t")
863
864 old_time = _now() - datetime.timedelta(days=100)
865 old_stale = dc_replace(old_stale, recorded_at=old_time)
866 old_resolved = dc_replace(old_resolved, recorded_at=old_time)
867
868 record_pattern(repo, old_stale)
869 record_pattern(repo, old_resolved)
870 record_pattern(repo, new_stale)
871
872 r = _make_resolution(old_resolved, b"res", repo)
873 save_resolution(repo, r)
874
875 removed = gc_stale(repo, age_days=90)
876 assert removed == 1
877 remaining = {p.pattern_id for p in list_patterns(repo)}
878 assert old_resolved.pattern_id in remaining
879 assert new_stale.pattern_id in remaining
880 assert old_stale.pattern_id not in remaining
881
882
883 # ===========================================================================
884 # 13. Audit log
885 # ===========================================================================
886
887
888 class TestAuditLog:
889 """append_audit(root, event_type, acted_by, *, pattern_id, resolution_id, policy_id, metadata)"""
890
891 def test_empty_store(self, repo: pathlib.Path) -> None:
892 assert list_audit(repo) == []
893
894 def test_append_and_list(self, repo: pathlib.Path) -> None:
895 append_audit(
896 repo,
897 AuditEventType.PATTERN_RECORDED,
898 AgentProvenance.human(),
899 pattern_id=_hex64("p"),
900 )
901 entries = list_audit(repo)
902 assert len(entries) == 1
903 assert entries[0]["event_type"] == AuditEventType.PATTERN_RECORDED
904
905 def test_multiple_entries_accumulate(self, repo: pathlib.Path) -> None:
906 for i in range(5):
907 append_audit(
908 repo,
909 AuditEventType.RESOLUTION_SAVED,
910 AgentProvenance.human(),
911 metadata={"i": i},
912 )
913 assert len(list_audit(repo)) == 5
914
915 def test_limit_parameter(self, repo: pathlib.Path) -> None:
916 for _ in range(10):
917 append_audit(repo, AuditEventType.PATTERN_RECORDED, AgentProvenance.human())
918 assert len(list_audit(repo, limit=3)) == 3
919
920 def test_audit_is_append_only(self, repo: pathlib.Path) -> None:
921 append_audit(repo, AuditEventType.PATTERN_RECORDED, AgentProvenance.human())
922 append_audit(repo, AuditEventType.RESOLUTION_APPLIED, AgentProvenance.human())
923 entries = list_audit(repo)
924 types = {e["event_type"] for e in entries}
925 assert AuditEventType.PATTERN_RECORDED in types
926 assert AuditEventType.RESOLUTION_APPLIED in types
927
928 def test_agent_provenance_in_audit(self, repo: pathlib.Path) -> None:
929 prov = AgentProvenance.agent("claude-code", "claude-sonnet-4-6")
930 append_audit(repo, AuditEventType.RESOLUTION_APPLIED, prov)
931 entries = list_audit(repo)
932 assert len(entries) == 1
933 assert entries[0]["acted_by"]["type"] == "agent"
934 assert entries[0]["acted_by"]["agent_id"] == "claude-code"
935
936 def test_different_event_types(self, repo: pathlib.Path) -> None:
937 for event in [
938 AuditEventType.PATTERN_RECORDED,
939 AuditEventType.RESOLUTION_SAVED,
940 AuditEventType.RESOLUTION_APPLIED,
941 AuditEventType.GC_RUN,
942 AuditEventType.CLEAR_RUN,
943 ]:
944 append_audit(repo, event, AgentProvenance.human())
945 entries = list_audit(repo, limit=100)
946 event_types = {e["event_type"] for e in entries}
947 for event in [
948 AuditEventType.PATTERN_RECORDED,
949 AuditEventType.RESOLUTION_SAVED,
950 AuditEventType.RESOLUTION_APPLIED,
951 ]:
952 assert event in event_types
953
954 def test_metadata_stored(self, repo: pathlib.Path) -> None:
955 append_audit(
956 repo,
957 AuditEventType.PATTERN_RECORDED,
958 AgentProvenance.human(),
959 pattern_id=_hex64("p"),
960 metadata={"extra": "value"},
961 )
962 entries = list_audit(repo)
963 assert entries[0]["metadata"]["extra"] == "value"
964
965 def test_audit_id_is_content_addressed(self, repo: pathlib.Path) -> None:
966 """audit_id must be sha256: of canonical entry content, not a UUID."""
967 import json as _json
968 append_audit(
969 repo,
970 AuditEventType.PATTERN_RECORDED,
971 AgentProvenance.human(),
972 pattern_id=_hex64("p"),
973 metadata={"k": "v"},
974 )
975 entry = list_audit(repo)[0]
976 audit_id = entry["audit_id"]
977 # Must be a long_id, not a UUID4
978 assert audit_id.startswith("sha256:"), f"Expected sha256: prefix, got {audit_id!r}"
979 assert len(audit_id) == 71, f"Expected 71 chars (sha256: + 64 hex), got {len(audit_id)}"
980
981 def test_audit_id_is_deterministic(self, repo: pathlib.Path) -> None:
982 """Same content → same audit_id (content-addressed, not random)."""
983 import json as _json
984 prov = AgentProvenance.human()
985 # Compute what the id should be from the entry fields
986 append_audit(
987 repo,
988 AuditEventType.RESOLUTION_SAVED,
989 prov,
990 pattern_id=_hex64("p"),
991 resolution_id=_hex64("r"),
992 metadata={"x": 1},
993 )
994 entry = list_audit(repo)[0]
995 # Re-derive: sha256 of entry without audit_id, sorted keys
996 payload = {k: v for k, v in entry.items() if k != "audit_id"}
997 expected = blob_id(_json.dumps(payload, sort_keys=True, separators=(",", ":")).encode())
998 assert entry["audit_id"] == expected
999
1000 def test_audit_id_not_uuid(self, repo: pathlib.Path) -> None:
1001 """audit_id must not be a UUID4 (8-4-4-4-12 format)."""
1002 import re
1003 append_audit(repo, AuditEventType.GC_RUN, AgentProvenance.human())
1004 entry = list_audit(repo)[0]
1005 uuid_re = re.compile(
1006 r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
1007 )
1008 assert not uuid_re.match(entry["audit_id"]), "audit_id must not be a UUID4"
1009
1010
1011 # ===========================================================================
1012 # 14. Policy CRUD
1013 # ===========================================================================
1014
1015
1016 class TestPolicyCRUD:
1017 def test_save_and_load(self, repo: pathlib.Path) -> None:
1018 policy = _make_policy("my-policy")
1019 save_policy(repo, policy)
1020 loaded = load_policy(repo, "my-policy")
1021 assert loaded is not None
1022 assert loaded.policy_id == "my-policy"
1023 assert loaded.action == PolicyAction.PREFER_OURS
1024
1025 def test_load_missing_returns_none(self, repo: pathlib.Path) -> None:
1026 assert load_policy(repo, "nonexistent") is None
1027
1028 def test_load_invalid_id_returns_none(self, repo: pathlib.Path) -> None:
1029 assert load_policy(repo, "bad id!") is None
1030
1031 def test_save_overwrites_existing(self, repo: pathlib.Path) -> None:
1032 p1 = _make_policy("pol", action=PolicyAction.PREFER_OURS)
1033 save_policy(repo, p1)
1034 p2 = _make_policy("pol", action=PolicyAction.PREFER_THEIRS)
1035 save_policy(repo, p2)
1036 loaded = load_policy(repo, "pol")
1037 assert loaded is not None
1038 assert loaded.action == PolicyAction.PREFER_THEIRS
1039
1040 def test_list_empty(self, repo: pathlib.Path) -> None:
1041 assert list_policies(repo) == []
1042
1043 def test_list_multiple(self, repo: pathlib.Path) -> None:
1044 for pid in ["a", "b", "c"]:
1045 save_policy(repo, _make_policy(pid))
1046 assert len(list_policies(repo)) == 3
1047
1048 def test_remove_existing(self, repo: pathlib.Path) -> None:
1049 save_policy(repo, _make_policy("remove-me"))
1050 assert remove_policy(repo, "remove-me") is True
1051 assert load_policy(repo, "remove-me") is None
1052
1053 def test_remove_missing_returns_false(self, repo: pathlib.Path) -> None:
1054 assert remove_policy(repo, "nope") is False
1055
1056 def test_list_sorted_by_scope_order(self, repo: pathlib.Path) -> None:
1057 file_pol = _make_policy("file-pol", scope=PolicyScope.FILE)
1058 ws_pol = _make_policy("ws-pol", scope=PolicyScope.WORKSPACE)
1059 domain_pol = _make_policy("domain-pol", scope=PolicyScope.DOMAIN)
1060 repo_pol = _make_policy("repo-pol", scope=PolicyScope.REPO)
1061
1062 for p in [file_pol, domain_pol, ws_pol, repo_pol]:
1063 save_policy(repo, p)
1064
1065 ordered = list_policies(repo)
1066 scopes = [p.scope for p in ordered]
1067 expected_order = [PolicyScope.WORKSPACE, PolicyScope.REPO,
1068 PolicyScope.DOMAIN, PolicyScope.FILE]
1069 assert scopes == expected_order
1070
1071
1072 # ===========================================================================
1073 # 15. PolicyCondition matching (_condition_matches and match_policy)
1074 # ===========================================================================
1075
1076
1077 class TestPolicyConditionMatching:
1078 """_condition_matches and match_policy — all predicate combinations."""
1079
1080 def _pattern(self, path: str = "f.py", domain: str = "code",
1081 conflict_type: str = ConflictType.CONTENT) -> ConflictPattern:
1082 blob = _hex64("fp")
1083 pid = compute_pattern_id(path, blob, blob)
1084 return ConflictPattern(
1085 pattern_id=pid,
1086 path=path,
1087 domain=domain,
1088 conflict_type=conflict_type,
1089 blob_fingerprint=blob,
1090 semantic_fingerprint=blob,
1091 ours_id=blob_id(b"o"),
1092 theirs_id=blob_id(b"t"),
1093 description={},
1094 recorded_at=_now(),
1095 recorded_by="test",
1096 )
1097
1098 def test_all_none_matches_everything(self) -> None:
1099 cond = PolicyCondition()
1100 assert h._condition_matches(cond, self._pattern())
1101 assert h._condition_matches(cond, self._pattern(domain="midi"))
1102
1103 def test_conflict_type_match(self) -> None:
1104 cond = PolicyCondition(conflict_type=ConflictType.CONTENT)
1105 assert h._condition_matches(cond, self._pattern())
1106
1107 def test_conflict_type_no_match(self) -> None:
1108 cond = PolicyCondition(conflict_type=ConflictType.STRUCTURAL)
1109 assert not h._condition_matches(cond, self._pattern())
1110
1111 def test_domain_match(self) -> None:
1112 cond = PolicyCondition(domain="code")
1113 assert h._condition_matches(cond, self._pattern(domain="code"))
1114
1115 def test_domain_no_match(self) -> None:
1116 cond = PolicyCondition(domain="midi")
1117 assert not h._condition_matches(cond, self._pattern(domain="code"))
1118
1119 def test_path_pattern_exact_glob(self) -> None:
1120 cond = PolicyCondition(path_pattern="*.py")
1121 assert h._condition_matches(cond, self._pattern(path="app.py"))
1122
1123 def test_path_pattern_directory_glob(self) -> None:
1124 cond = PolicyCondition(path_pattern="src/*.py")
1125 assert h._condition_matches(cond, self._pattern(path="src/main.py"))
1126 assert not h._condition_matches(cond, self._pattern(path="tests/main.py"))
1127
1128 def test_path_pattern_no_match(self) -> None:
1129 cond = PolicyCondition(path_pattern="*.mid")
1130 assert not h._condition_matches(cond, self._pattern(path="song.py"))
1131
1132 def test_all_conditions_must_match(self) -> None:
1133 cond = PolicyCondition(conflict_type=ConflictType.CONTENT, domain="code",
1134 path_pattern="*.py")
1135 assert h._condition_matches(cond, self._pattern())
1136 # domain wrong
1137 assert not h._condition_matches(cond, self._pattern(domain="midi"))
1138 # path wrong
1139 assert not h._condition_matches(cond, self._pattern(path="song.mid"))
1140
1141 def test_min_confidence_not_checked_here(self) -> None:
1142 """min_confidence is a proposal-time filter — _condition_matches ignores it."""
1143 cond = PolicyCondition(min_confidence=0.99)
1144 # Should match regardless — min_confidence is not a pattern field
1145 assert h._condition_matches(cond, self._pattern())
1146
1147 def test_match_policy_returns_none_when_no_policies(self) -> None:
1148 assert match_policy([], self._pattern()) is None
1149
1150 def test_match_policy_returns_first_match(self) -> None:
1151 p1 = _make_policy("p1", action=PolicyAction.PREFER_OURS,
1152 conflict_type=ConflictType.CONTENT)
1153 p2 = _make_policy("p2", action=PolicyAction.PREFER_THEIRS,
1154 conflict_type=ConflictType.CONTENT)
1155 result = match_policy([p1, p2], self._pattern())
1156 assert result is not None
1157 assert result.policy_id == "p1"
1158
1159 def test_match_policy_returns_none_when_no_match(self) -> None:
1160 p1 = _make_policy("p1", domain="midi") # won't match domain="code"
1161 result = match_policy([p1], self._pattern(domain="code"))
1162 assert result is None
1163
1164 def test_match_policy_skips_non_matching(self) -> None:
1165 wrong = _make_policy("wrong", domain="midi")
1166 right = _make_policy("right", domain="code")
1167 result = match_policy([wrong, right], self._pattern(domain="code"))
1168 assert result is not None
1169 assert result.policy_id == "right"
1170
1171 def test_match_policy_symbol_path_glob(self) -> None:
1172 """path_pattern must match symbol-level paths like 'config.py::*'."""
1173 cond_pol = _make_policy("sym", path_pattern="config.py::*")
1174 result = match_policy([cond_pol], self._pattern(path="config.py::MAX_CONNECTIONS"))
1175 assert result is not None
1176
1177
1178 # ===========================================================================
1179 # 16. Escalation lifecycle
1180 # ===========================================================================
1181
1182
1183 class TestEscalationLifecycle:
1184 def _make_escalation(self, pattern_id: str | None = None) -> EscalationRecord:
1185 pid = pattern_id or _hex64("pat")
1186 reason = "could not auto-resolve"
1187 eid = compute_escalation_id(pid, reason)
1188 return EscalationRecord(
1189 escalation_id=eid,
1190 pattern_id=pid,
1191 reason=reason,
1192 escalated_at=_now(),
1193 escalated_by=AgentProvenance.agent("claude-code"),
1194 status=EscalationStatus.OPEN,
1195 )
1196
1197 def test_compute_escalation_id_deterministic(self) -> None:
1198 pid = _hex64("p")
1199 reason = "reason"
1200 assert compute_escalation_id(pid, reason) == compute_escalation_id(pid, reason)
1201
1202 def test_compute_escalation_id_differs_on_different_inputs(self) -> None:
1203 pid = _hex64("p")
1204 assert compute_escalation_id(pid, "r1") != compute_escalation_id(pid, "r2")
1205 assert compute_escalation_id(_hex64("p1"), "r") != compute_escalation_id(_hex64("p2"), "r")
1206
1207 def test_record_and_load(self, repo: pathlib.Path) -> None:
1208 rec = self._make_escalation()
1209 result = record_escalation(repo, rec)
1210 assert result is True
1211 loaded = load_escalation(repo, rec.escalation_id)
1212 assert loaded is not None
1213 assert loaded.escalation_id == rec.escalation_id
1214 assert loaded.status == EscalationStatus.OPEN
1215
1216 def test_record_idempotent(self, repo: pathlib.Path) -> None:
1217 rec = self._make_escalation()
1218 assert record_escalation(repo, rec) is True
1219 assert record_escalation(repo, rec) is False # already exists
1220
1221 def test_load_missing_returns_none(self, repo: pathlib.Path) -> None:
1222 assert load_escalation(repo, _hex64("gone")) is None
1223
1224 def test_list_escalations_empty(self, repo: pathlib.Path) -> None:
1225 assert list_escalations(repo) == []
1226
1227 def test_list_escalations_all(self, repo: pathlib.Path) -> None:
1228 for i in range(3):
1229 rec = self._make_escalation(_hex64(f"pat{i}"))
1230 record_escalation(repo, rec)
1231 assert len(list_escalations(repo)) == 3
1232
1233 def test_list_escalations_filter_by_status(self, repo: pathlib.Path) -> None:
1234 open_rec = self._make_escalation(_hex64("open"))
1235 record_escalation(repo, open_rec)
1236
1237 resolved_rec = self._make_escalation(_hex64("resolved"))
1238 record_escalation(repo, resolved_rec)
1239 resolve_escalation(
1240 repo,
1241 resolved_rec.escalation_id,
1242 _hex64("res"),
1243 AgentProvenance.human(),
1244 _now(),
1245 )
1246
1247 open_list = list_escalations(repo, status=EscalationStatus.OPEN)
1248 resolved_list = list_escalations(repo, status=EscalationStatus.RESOLVED)
1249 assert len(open_list) == 1
1250 assert open_list[0].status == EscalationStatus.OPEN
1251 assert len(resolved_list) == 1
1252 assert resolved_list[0].status == EscalationStatus.RESOLVED
1253
1254 def test_resolve_escalation(self, repo: pathlib.Path) -> None:
1255 rec = self._make_escalation()
1256 record_escalation(repo, rec)
1257
1258 result = resolve_escalation(
1259 repo,
1260 rec.escalation_id,
1261 _hex64("resolution"),
1262 AgentProvenance.human(),
1263 _now(),
1264 )
1265 assert result is True
1266
1267 loaded = load_escalation(repo, rec.escalation_id)
1268 assert loaded is not None
1269 assert loaded.status == EscalationStatus.RESOLVED
1270 assert loaded.resolution_id == _hex64("resolution")
1271
1272 def test_resolve_missing_escalation_returns_false(self, repo: pathlib.Path) -> None:
1273 result = resolve_escalation(
1274 repo, _hex64("gone"), _hex64("res"), AgentProvenance.human(), _now()
1275 )
1276 assert result is False
1277
1278
1279 # ===========================================================================
1280 # 17. record_resolutions — file-level paths
1281 # ===========================================================================
1282
1283
1284 class TestRecordResolutionsFilePaths:
1285 def test_records_pattern_and_resolution(self, repo: pathlib.Path) -> None:
1286 ours_id = _write_obj(repo, b"version = 1")
1287 theirs_id = _write_obj(repo, b"version = 2")
1288 resolution_id = _write_obj(repo, b"version = 3")
1289
1290 ours_m: Manifest = {"config.py": ours_id}
1291 theirs_m: Manifest = {"config.py": theirs_id}
1292 new_m: Manifest = {"config.py": resolution_id}
1293
1294 saved = record_resolutions(repo, ["config.py"], ours_m, theirs_m, new_m, "code", _FakePlugin())
1295 assert saved == ["config.py"]
1296
1297 patterns = list_patterns(repo)
1298 assert len(patterns) == 1
1299 assert patterns[0].path == "config.py"
1300
1301 resolutions = list_resolutions(repo, patterns[0].pattern_id)
1302 assert len(resolutions) == 1
1303 assert resolutions[0].outcome_blob == resolution_id
1304 assert resolutions[0].human_verified is True
1305 assert resolutions[0].confidence == 1.0
1306 assert resolutions[0].strategy == ResolutionStrategy.MANUAL
1307
1308 def test_skips_path_not_in_manifests(self, repo: pathlib.Path) -> None:
1309 saved = record_resolutions(repo, ["missing.py"], {}, {}, {}, "code", _FakePlugin())
1310 assert saved == []
1311 assert list_patterns(repo) == []
1312
1313 def test_skips_when_ours_missing_from_manifest(self, repo: pathlib.Path) -> None:
1314 theirs_id = _write_obj(repo, b"v2")
1315 res_id = _write_obj(repo, b"v2")
1316 saved = record_resolutions(
1317 repo, ["f.py"], {}, {"f.py": theirs_id}, {"f.py": res_id}, "code", _FakePlugin()
1318 )
1319 assert saved == []
1320
1321 def test_skips_when_theirs_missing_from_manifest(self, repo: pathlib.Path) -> None:
1322 ours_id = _write_obj(repo, b"v1")
1323 res_id = _write_obj(repo, b"v1")
1324 saved = record_resolutions(
1325 repo, ["f.py"], {"f.py": ours_id}, {}, {"f.py": res_id}, "code", _FakePlugin()
1326 )
1327 assert saved == []
1328
1329 def test_skips_when_outcome_missing_from_new_manifest(self, repo: pathlib.Path) -> None:
1330 ours_id = _write_obj(repo, b"v1")
1331 theirs_id = _write_obj(repo, b"v2")
1332 saved = record_resolutions(
1333 repo, ["f.py"], {"f.py": ours_id}, {"f.py": theirs_id}, {}, "code", _FakePlugin()
1334 )
1335 assert saved == []
1336
1337 def test_idempotent_second_call(self, repo: pathlib.Path) -> None:
1338 ours_id = _write_obj(repo, b"a")
1339 theirs_id = _write_obj(repo, b"b")
1340 resolution_id = _write_obj(repo, b"c")
1341 ours_m: Manifest = {"f.py": ours_id}
1342 theirs_m: Manifest = {"f.py": theirs_id}
1343 new_m: Manifest = {"f.py": resolution_id}
1344
1345 record_resolutions(repo, ["f.py"], ours_m, theirs_m, new_m, "code", _FakePlugin())
1346 record_resolutions(repo, ["f.py"], ours_m, theirs_m, new_m, "code", _FakePlugin())
1347
1348 assert len(list_patterns(repo)) == 1
1349 assert len(list_resolutions(repo, list_patterns(repo)[0].pattern_id)) == 1
1350
1351 def test_multiple_paths(self, repo: pathlib.Path) -> None:
1352 ours_a = _write_obj(repo, b"a_ours")
1353 theirs_a = _write_obj(repo, b"a_theirs")
1354 res_a = _write_obj(repo, b"a_res")
1355
1356 ours_b = _write_obj(repo, b"b_ours")
1357 theirs_b = _write_obj(repo, b"b_theirs")
1358 res_b = _write_obj(repo, b"b_res")
1359
1360 saved = record_resolutions(
1361 repo,
1362 ["a.py", "b.py"],
1363 {"a.py": ours_a, "b.py": ours_b},
1364 {"a.py": theirs_a, "b.py": theirs_b},
1365 {"a.py": res_a, "b.py": res_b},
1366 "code",
1367 _FakePlugin(),
1368 )
1369 assert set(saved) == {"a.py", "b.py"}
1370 assert len(list_patterns(repo)) == 2
1371
1372 def test_returns_only_saved_paths(self, repo: pathlib.Path) -> None:
1373 """Paths missing from manifests are silently skipped, not in return value."""
1374 ours_id = _write_obj(repo, b"x")
1375 theirs_id = _write_obj(repo, b"y")
1376 res_id = _write_obj(repo, b"z")
1377
1378 saved = record_resolutions(
1379 repo,
1380 ["present.py", "absent.py"],
1381 {"present.py": ours_id},
1382 {"present.py": theirs_id},
1383 {"present.py": res_id},
1384 "code",
1385 _FakePlugin(),
1386 )
1387 assert saved == ["present.py"]
1388
1389
1390 # ===========================================================================
1391 # 18. record_resolutions — symbol-level paths
1392 # ===========================================================================
1393
1394
1395 class TestRecordResolutionsSymbolPaths:
1396 def test_symbol_path_records_pattern(self, repo: pathlib.Path) -> None:
1397 ours_id = _write_obj(repo, b"MAX_CONNECTIONS = 10")
1398 theirs_id = _write_obj(repo, b"MAX_CONNECTIONS = 25")
1399 resolution_id = _write_obj(repo, b"MAX_CONNECTIONS = 50")
1400
1401 saved = record_resolutions(
1402 repo,
1403 ["config.py::MAX_CONNECTIONS"],
1404 {"config.py": ours_id},
1405 {"config.py": theirs_id},
1406 {"config.py": resolution_id},
1407 "code",
1408 _FakePlugin(),
1409 )
1410
1411 assert saved == ["config.py::MAX_CONNECTIONS"]
1412 patterns = list_patterns(repo)
1413 assert len(patterns) == 1
1414 assert patterns[0].path == "config.py::MAX_CONNECTIONS"
1415
1416 def test_multiple_symbols_same_file_produce_distinct_patterns(
1417 self, repo: pathlib.Path
1418 ) -> None:
1419 file_ours = _write_obj(repo, b"file-ours")
1420 file_theirs = _write_obj(repo, b"file-theirs")
1421 file_resolved = _write_obj(repo, b"file-resolved")
1422
1423 saved = record_resolutions(
1424 repo,
1425 ["app.py::foo", "app.py::bar"],
1426 {"app.py": file_ours},
1427 {"app.py": file_theirs},
1428 {"app.py": file_resolved},
1429 "code",
1430 _FakePlugin(),
1431 )
1432 assert set(saved) == {"app.py::foo", "app.py::bar"}
1433 patterns = list_patterns(repo)
1434 assert len(patterns) == 2
1435 paths = {p.path for p in patterns}
1436 assert paths == {"app.py::foo", "app.py::bar"}
1437
1438 def test_symbol_path_missing_file_portion(self, repo: pathlib.Path) -> None:
1439 saved = record_resolutions(
1440 repo, ["missing.py::Symbol"], {}, {}, {}, "code", _FakePlugin()
1441 )
1442 assert saved == []
1443
1444 def test_symbol_path_idempotent(self, repo: pathlib.Path) -> None:
1445 ours_id = _write_obj(repo, b"ours")
1446 theirs_id = _write_obj(repo, b"theirs")
1447 res_id = _write_obj(repo, b"resolved")
1448 ours_m: Manifest = {"f.py": ours_id}
1449 theirs_m: Manifest = {"f.py": theirs_id}
1450 new_m: Manifest = {"f.py": res_id}
1451
1452 record_resolutions(repo, ["f.py::Symbol"], ours_m, theirs_m, new_m, "code", _FakePlugin())
1453 record_resolutions(repo, ["f.py::Symbol"], ours_m, theirs_m, new_m, "code", _FakePlugin())
1454
1455 assert len(list_patterns(repo)) == 1
1456 assert len(list_resolutions(repo, list_patterns(repo)[0].pattern_id)) == 1
1457
1458 def test_deeply_nested_symbol_path(self, repo: pathlib.Path) -> None:
1459 ours_id = _write_obj(repo, b"ours")
1460 theirs_id = _write_obj(repo, b"theirs")
1461 res_id = _write_obj(repo, b"resolved")
1462
1463 saved = record_resolutions(
1464 repo,
1465 ["src/auth/tokens.py::TokenManager.rotate"],
1466 {"src/auth/tokens.py": ours_id},
1467 {"src/auth/tokens.py": theirs_id},
1468 {"src/auth/tokens.py": res_id},
1469 "code",
1470 _FakePlugin(),
1471 )
1472 assert saved == ["src/auth/tokens.py::TokenManager.rotate"]
1473 patterns = list_patterns(repo)
1474 assert patterns[0].path == "src/auth/tokens.py::TokenManager.rotate"
1475
1476
1477 # ===========================================================================
1478 # 19. auto_apply — exact replay (file paths)
1479 # ===========================================================================
1480
1481
1482 class TestAutoApplyExactReplay:
1483 def test_no_resolution_records_pattern_and_returns_remaining(
1484 self, repo: pathlib.Path
1485 ) -> None:
1486 ours_id = _write_obj(repo, b"v1")
1487 theirs_id = _write_obj(repo, b"v2")
1488 ours_m: Manifest = {"f.py": ours_id}
1489 theirs_m: Manifest = {"f.py": theirs_id}
1490
1491 resolved, remaining = auto_apply(
1492 repo, ["f.py"], ours_m, theirs_m, "code", _FakePlugin()
1493 )
1494 assert resolved == {}
1495 assert "f.py" in remaining
1496 # Pattern should have been recorded for future learning
1497 assert len(list_patterns(repo)) == 1
1498
1499 def test_second_identical_conflict_auto_resolves(
1500 self, repo: pathlib.Path
1501 ) -> None:
1502 ours_id = _write_obj(repo, b"ours-content")
1503 theirs_id = _write_obj(repo, b"theirs-content")
1504 resolution_content = b"resolved-content"
1505 resolution_id = _write_obj(repo, resolution_content)
1506
1507 ours_m: Manifest = {"f.py": ours_id}
1508 theirs_m: Manifest = {"f.py": theirs_id}
1509 new_m: Manifest = {"f.py": resolution_id}
1510
1511 # First conflict: record how it was resolved
1512 record_resolutions(repo, ["f.py"], ours_m, theirs_m, new_m, "code", _FakePlugin())
1513
1514 # Second identical conflict: auto_apply should replay
1515 dest = repo / "f.py"
1516 resolved, remaining = auto_apply(repo, ["f.py"], ours_m, theirs_m, "code", _FakePlugin())
1517
1518 assert "f.py" in resolved
1519 assert remaining == []
1520 assert dest.read_bytes() == resolution_content
1521
1522 def test_commutative_replay(self, repo: pathlib.Path) -> None:
1523 """Record A-vs-B; auto_apply B-vs-A should still match."""
1524 ours_id = _write_obj(repo, b"A")
1525 theirs_id = _write_obj(repo, b"B")
1526 resolution_id = _write_obj(repo, b"A") # kept ours
1527
1528 ours_m: Manifest = {"f.py": ours_id}
1529 theirs_m: Manifest = {"f.py": theirs_id}
1530 new_m: Manifest = {"f.py": resolution_id}
1531
1532 record_resolutions(repo, ["f.py"], ours_m, theirs_m, new_m, "code", _FakePlugin())
1533
1534 # Swapped: ours=B, theirs=A — same fingerprint (commutative)
1535 swapped_ours: Manifest = {"f.py": theirs_id}
1536 swapped_theirs: Manifest = {"f.py": ours_id}
1537 resolved, remaining = auto_apply(
1538 repo, ["f.py"], swapped_ours, swapped_theirs, "code", _FakePlugin()
1539 )
1540 assert "f.py" in resolved
1541 assert remaining == []
1542
1543 def test_different_content_does_not_auto_resolve(self, repo: pathlib.Path) -> None:
1544 """Different blob IDs → different fingerprint → no auto-apply."""
1545 ours_id = _write_obj(repo, b"old-ours")
1546 theirs_id = _write_obj(repo, b"old-theirs")
1547 resolution_id = _write_obj(repo, b"old-resolved")
1548
1549 record_resolutions(
1550 repo, ["f.py"],
1551 {"f.py": ours_id}, {"f.py": theirs_id}, {"f.py": resolution_id},
1552 "code", _FakePlugin(),
1553 )
1554
1555 new_ours = _write_obj(repo, b"new-ours")
1556 new_theirs = _write_obj(repo, b"new-theirs")
1557
1558 resolved, remaining = auto_apply(
1559 repo, ["f.py"],
1560 {"f.py": new_ours}, {"f.py": new_theirs},
1561 "code", _FakePlugin(),
1562 )
1563 assert resolved == {}
1564 assert "f.py" in remaining
1565
1566 def test_applied_count_incremented_on_replay(self, repo: pathlib.Path) -> None:
1567 ours_id = _write_obj(repo, b"ours")
1568 theirs_id = _write_obj(repo, b"theirs")
1569 resolution_id = _write_obj(repo, b"resolved")
1570
1571 ours_m: Manifest = {"f.py": ours_id}
1572 theirs_m: Manifest = {"f.py": theirs_id}
1573 new_m: Manifest = {"f.py": resolution_id}
1574 record_resolutions(repo, ["f.py"], ours_m, theirs_m, new_m, "code", _FakePlugin())
1575
1576 auto_apply(repo, ["f.py"], ours_m, theirs_m, "code", _FakePlugin())
1577
1578 p = list_patterns(repo)[0]
1579 resolutions = list_resolutions(repo, p.pattern_id)
1580 assert resolutions[0].applied_count == 1
1581
1582 def test_multiple_replays_increment_count(self, repo: pathlib.Path) -> None:
1583 ours_id = _write_obj(repo, b"ours")
1584 theirs_id = _write_obj(repo, b"theirs")
1585 resolution_id = _write_obj(repo, b"resolved")
1586
1587 ours_m: Manifest = {"f.py": ours_id}
1588 theirs_m: Manifest = {"f.py": theirs_id}
1589 new_m: Manifest = {"f.py": resolution_id}
1590 record_resolutions(repo, ["f.py"], ours_m, theirs_m, new_m, "code", _FakePlugin())
1591
1592 for _ in range(3):
1593 auto_apply(repo, ["f.py"], ours_m, theirs_m, "code", _FakePlugin())
1594
1595 p = list_patterns(repo)[0]
1596 resolutions = list_resolutions(repo, p.pattern_id)
1597 assert resolutions[0].applied_count == 3
1598
1599 def test_resolves_file_to_disk(self, repo: pathlib.Path) -> None:
1600 """The resolved content must actually be written to the working tree."""
1601 ours_id = _write_obj(repo, b"ours")
1602 theirs_id = _write_obj(repo, b"theirs")
1603 content = b"the chosen resolution\n"
1604 resolution_id = _write_obj(repo, content)
1605
1606 ours_m: Manifest = {"result.py": ours_id}
1607 theirs_m: Manifest = {"result.py": theirs_id}
1608 new_m: Manifest = {"result.py": resolution_id}
1609 record_resolutions(repo, ["result.py"], ours_m, theirs_m, new_m, "code", _FakePlugin())
1610
1611 dest = repo / "result.py"
1612 auto_apply(repo, ["result.py"], ours_m, theirs_m, "code", _FakePlugin())
1613 assert dest.read_bytes() == content
1614
1615
1616 # ===========================================================================
1617 # 20. auto_apply — symbol-level paths
1618 # ===========================================================================
1619
1620
1621 class TestAutoApplySymbolPaths:
1622 def test_first_symbol_conflict_records_pattern(self, repo: pathlib.Path) -> None:
1623 ours_id = _write_obj(repo, b"DEBUG = False")
1624 theirs_id = _write_obj(repo, b"DEBUG = True")
1625
1626 _, remaining = auto_apply(
1627 repo,
1628 ["settings.py::DEBUG"],
1629 {"settings.py": ours_id},
1630 {"settings.py": theirs_id},
1631 "code",
1632 _FakePlugin(),
1633 )
1634
1635 assert "settings.py::DEBUG" in remaining
1636 patterns = list_patterns(repo)
1637 assert len(patterns) == 1
1638 assert patterns[0].path == "settings.py::DEBUG"
1639
1640 def test_symbol_conflict_replayed(self, repo: pathlib.Path) -> None:
1641 ours_id = _write_obj(repo, b"TIMEOUT = 30")
1642 theirs_id = _write_obj(repo, b"TIMEOUT = 60")
1643 resolution_content = b"TIMEOUT = 45"
1644 resolution_id = _write_obj(repo, resolution_content)
1645
1646 ours_m: Manifest = {"config.py": ours_id}
1647 theirs_m: Manifest = {"config.py": theirs_id}
1648 new_m: Manifest = {"config.py": resolution_id}
1649
1650 record_resolutions(
1651 repo, ["config.py::TIMEOUT"], ours_m, theirs_m, new_m, "code", _FakePlugin()
1652 )
1653
1654 dest = repo / "config.py"
1655 resolved, remaining = auto_apply(
1656 repo, ["config.py::TIMEOUT"], ours_m, theirs_m, "code", _FakePlugin()
1657 )
1658
1659 assert "config.py::TIMEOUT" in resolved
1660 assert remaining == []
1661 assert dest.read_bytes() == resolution_content
1662
1663 def test_symbol_and_file_path_produce_distinct_patterns(
1664 self, repo: pathlib.Path
1665 ) -> None:
1666 """config.py and config.py::MAX must produce separate patterns."""
1667 ours_id = _write_obj(repo, b"ours")
1668 theirs_id = _write_obj(repo, b"theirs")
1669 res_id = _write_obj(repo, b"res")
1670
1671 ours_m: Manifest = {"config.py": ours_id}
1672 theirs_m: Manifest = {"config.py": theirs_id}
1673 new_m: Manifest = {"config.py": res_id}
1674
1675 record_resolutions(repo, ["config.py"], ours_m, theirs_m, new_m, "code", _FakePlugin())
1676 auto_apply(repo, ["config.py::MAX"], ours_m, theirs_m, "code", _FakePlugin())
1677
1678 patterns = list_patterns(repo)
1679 assert len(patterns) == 2
1680 paths = {p.path for p in patterns}
1681 assert "config.py" in paths
1682 assert "config.py::MAX" in paths
1683
1684 def test_multiple_symbols_in_one_auto_apply(self, repo: pathlib.Path) -> None:
1685 ours_id = _write_obj(repo, b"ours")
1686 theirs_id = _write_obj(repo, b"theirs")
1687 res_id = _write_obj(repo, b"res")
1688
1689 ours_m: Manifest = {"f.py": ours_id}
1690 theirs_m: Manifest = {"f.py": theirs_id}
1691 new_m: Manifest = {"f.py": res_id}
1692
1693 record_resolutions(
1694 repo, ["f.py::alpha", "f.py::beta"], ours_m, theirs_m, new_m, "code", _FakePlugin()
1695 )
1696 resolved, remaining = auto_apply(
1697 repo, ["f.py::alpha", "f.py::beta"], ours_m, theirs_m, "code", _FakePlugin()
1698 )
1699 assert "f.py::alpha" in resolved
1700 assert "f.py::beta" in resolved
1701 assert remaining == []
1702
1703 def test_partial_resolution_some_symbols_remain(
1704 self, repo: pathlib.Path
1705 ) -> None:
1706 """Only symbols with saved resolutions are auto-applied."""
1707 ours_id = _write_obj(repo, b"ours")
1708 theirs_id = _write_obj(repo, b"theirs")
1709 res_id = _write_obj(repo, b"res")
1710
1711 ours_m: Manifest = {"f.py": ours_id}
1712 theirs_m: Manifest = {"f.py": theirs_id}
1713 new_m: Manifest = {"f.py": res_id}
1714
1715 # Only record resolution for alpha, not beta
1716 record_resolutions(
1717 repo, ["f.py::alpha"], ours_m, theirs_m, new_m, "code", _FakePlugin()
1718 )
1719 resolved, remaining = auto_apply(
1720 repo, ["f.py::alpha", "f.py::beta"], ours_m, theirs_m, "code", _FakePlugin()
1721 )
1722 assert "f.py::alpha" in resolved
1723 assert "f.py::beta" in remaining
1724
1725
1726 # ===========================================================================
1727 # 21. auto_apply — path traversal guard
1728 # ===========================================================================
1729
1730
1731 class TestAutoApplyPathTraversal:
1732 def test_traversal_path_skipped(self, repo: pathlib.Path) -> None:
1733 ours_id = _write_obj(repo, b"x")
1734 theirs_id = _write_obj(repo, b"y")
1735 ours_m: Manifest = {"../evil.py": ours_id}
1736 theirs_m: Manifest = {"../evil.py": theirs_id}
1737
1738 resolved, remaining = auto_apply(
1739 repo, ["../evil.py"], ours_m, theirs_m, "code", _FakePlugin()
1740 )
1741 assert resolved == {}
1742 assert "../evil.py" in remaining
1743 # No pattern should have been recorded for a traversal attempt
1744 assert list_patterns(repo) == []
1745
1746 def test_symbol_traversal_skipped(self, repo: pathlib.Path) -> None:
1747 ours_id = _write_obj(repo, b"x")
1748 theirs_id = _write_obj(repo, b"y")
1749 ours_m: Manifest = {"../evil.py": ours_id}
1750 theirs_m: Manifest = {"../evil.py": theirs_id}
1751
1752 resolved, remaining = auto_apply(
1753 repo, ["../evil.py::Symbol"], ours_m, theirs_m, "code", _FakePlugin()
1754 )
1755 assert resolved == {}
1756 assert "../evil.py::Symbol" in remaining
1757 assert list_patterns(repo) == []
1758
1759 def test_absolute_path_skipped(self, repo: pathlib.Path) -> None:
1760 ours_id = _write_obj(repo, b"x")
1761 theirs_id = _write_obj(repo, b"y")
1762 abs_path = "/etc/passwd"
1763 ours_m: Manifest = {abs_path: ours_id}
1764 theirs_m: Manifest = {abs_path: theirs_id}
1765
1766 resolved, remaining = auto_apply(
1767 repo, [abs_path], ours_m, theirs_m, "code", _FakePlugin()
1768 )
1769 assert resolved == {}
1770 assert abs_path in remaining
1771
1772 def test_legitimate_nested_path_not_skipped(self, repo: pathlib.Path) -> None:
1773 ours_id = _write_obj(repo, b"v1")
1774 theirs_id = _write_obj(repo, b"v2")
1775 res_id = _write_obj(repo, b"v3")
1776 path = "src/auth/tokens.py"
1777 ours_m: Manifest = {path: ours_id}
1778 theirs_m: Manifest = {path: theirs_id}
1779 new_m: Manifest = {path: res_id}
1780
1781 record_resolutions(repo, [path], ours_m, theirs_m, new_m, "code", _FakePlugin())
1782 resolved, remaining = auto_apply(
1783 repo, [path], ours_m, theirs_m, "code", _FakePlugin()
1784 )
1785 assert path in resolved
1786 assert remaining == []
1787
1788
1789 # ===========================================================================
1790 # 22. auto_apply — one-sided deletion (ours or theirs missing)
1791 # ===========================================================================
1792
1793
1794 class TestAutoApplyOneWayDeletion:
1795 def test_ours_missing_from_manifest(self, repo: pathlib.Path) -> None:
1796 theirs_id = _write_obj(repo, b"theirs")
1797 resolved, remaining = auto_apply(
1798 repo, ["f.py"], {}, {"f.py": theirs_id}, "code", _FakePlugin()
1799 )
1800 assert resolved == {}
1801 assert "f.py" in remaining
1802
1803 def test_theirs_missing_from_manifest(self, repo: pathlib.Path) -> None:
1804 ours_id = _write_obj(repo, b"ours")
1805 resolved, remaining = auto_apply(
1806 repo, ["f.py"], {"f.py": ours_id}, {}, "code", _FakePlugin()
1807 )
1808 assert resolved == {}
1809 assert "f.py" in remaining
1810
1811 def test_both_missing(self, repo: pathlib.Path) -> None:
1812 resolved, remaining = auto_apply(
1813 repo, ["f.py"], {}, {}, "code", _FakePlugin()
1814 )
1815 assert resolved == {}
1816 assert "f.py" in remaining
1817
1818
1819 # ===========================================================================
1820 # 23. auto_apply — empty conflict list
1821 # ===========================================================================
1822
1823
1824 class TestAutoApplyEmptyList:
1825 def test_empty_conflict_list(self, repo: pathlib.Path) -> None:
1826 resolved, remaining = auto_apply(repo, [], {}, {}, "code", _FakePlugin())
1827 assert resolved == {}
1828 assert remaining == []
1829
1830 def test_empty_list_no_patterns_recorded(self, repo: pathlib.Path) -> None:
1831 auto_apply(repo, [], {}, {}, "code", _FakePlugin())
1832 assert list_patterns(repo) == []
1833
1834
1835 # ===========================================================================
1836 # 24. auto_apply — semantic fingerprinting via HarmonyPlugin
1837 # ===========================================================================
1838
1839
1840 class TestAutoApplySemanticPlugin:
1841 """A HarmonyPlugin that collapses semantically equivalent conflicts.
1842
1843 Scenario: Two conflicts that have different blob IDs but the plugin
1844 assigns them the same semantic fingerprint. Harmony should recognise
1845 the second conflict as a replay of the first.
1846 """
1847
1848 def test_semantic_plugin_enables_cross_content_replay(
1849 self, repo: pathlib.Path
1850 ) -> None:
1851 shared_semantic = _hex64("shared-semantic-fingerprint")
1852 plugin = _SemanticPlugin(shared_semantic)
1853
1854 # First conflict: blob pair (A, B)
1855 ours_A = _write_obj(repo, b"variant-A-ours")
1856 theirs_A = _write_obj(repo, b"variant-A-theirs")
1857 resolution_A = _write_obj(repo, b"resolution-A")
1858
1859 ours_m_A: Manifest = {"song.mid": ours_A}
1860 theirs_m_A: Manifest = {"song.mid": theirs_A}
1861 new_m_A: Manifest = {"song.mid": resolution_A}
1862
1863 record_resolutions(
1864 repo, ["song.mid"], ours_m_A, theirs_m_A, new_m_A, "midi", plugin
1865 )
1866
1867 # Second conflict: blob pair (C, D) — different content but same semantic FP
1868 ours_C = _write_obj(repo, b"variant-C-ours")
1869 theirs_C = _write_obj(repo, b"variant-C-theirs")
1870
1871 ours_m_C: Manifest = {"song.mid": ours_C}
1872 theirs_m_C: Manifest = {"song.mid": theirs_C}
1873
1874 dest = repo / "song.mid"
1875 resolved, remaining = auto_apply(
1876 repo, ["song.mid"], ours_m_C, theirs_m_C, "midi", plugin
1877 )
1878
1879 assert "song.mid" in resolved, (
1880 "Semantic plugin maps both conflicts to the same pattern_id; "
1881 "auto_apply must replay the saved resolution even though blob IDs differ"
1882 )
1883 assert remaining == []
1884 assert dest.read_bytes() == b"resolution-A"
1885
1886 def test_different_semantic_fingerprints_no_cross_replay(
1887 self, repo: pathlib.Path
1888 ) -> None:
1889 """Two semantically distinct conflicts must NOT cross-replay."""
1890 plugin_A = _SemanticPlugin(_hex64("semantic-A"))
1891 plugin_B = _SemanticPlugin(_hex64("semantic-B"))
1892
1893 ours_id = _write_obj(repo, b"ours")
1894 theirs_id = _write_obj(repo, b"theirs")
1895 res_id = _write_obj(repo, b"res")
1896
1897 ours_m: Manifest = {"f.py": ours_id}
1898 theirs_m: Manifest = {"f.py": theirs_id}
1899 new_m: Manifest = {"f.py": res_id}
1900
1901 record_resolutions(repo, ["f.py"], ours_m, theirs_m, new_m, "code", plugin_A)
1902
1903 # Same blobs, but plugin_B produces a different fingerprint
1904 resolved, remaining = auto_apply(
1905 repo, ["f.py"], ours_m, theirs_m, "code", plugin_B
1906 )
1907 assert resolved == {}
1908 assert "f.py" in remaining
1909
1910
1911 # ===========================================================================
1912 # 25. MergeState — original_conflict_paths (Bug 3)
1913 # ===========================================================================
1914
1915
1916 class TestMergeStateOriginalConflictPaths:
1917 def test_write_sets_original_conflict_paths(self, repo: pathlib.Path) -> None:
1918 write_merge_state(
1919 repo,
1920 base_commit=long_id("0" * 64),
1921 ours_commit=long_id("1" * 64),
1922 theirs_commit=long_id("2" * 64),
1923 conflict_paths=["config.py::MAX_CONNECTIONS", "utils.py::clamp"],
1924 )
1925 state = read_merge_state(repo)
1926 assert state is not None
1927 assert state.original_conflict_paths == [
1928 "config.py::MAX_CONNECTIONS",
1929 "utils.py::clamp",
1930 ]
1931
1932 def test_original_paths_preserved_after_partial_resolution(
1933 self, repo: pathlib.Path
1934 ) -> None:
1935 write_merge_state(
1936 repo,
1937 base_commit=long_id("0" * 64),
1938 ours_commit=long_id("1" * 64),
1939 theirs_commit=long_id("2" * 64),
1940 conflict_paths=["config.py::A", "config.py::B"],
1941 )
1942 write_merge_state(
1943 repo,
1944 base_commit=long_id("0" * 64),
1945 ours_commit=long_id("1" * 64),
1946 theirs_commit=long_id("2" * 64),
1947 conflict_paths=["config.py::B"], # A resolved
1948 )
1949 state = read_merge_state(repo)
1950 assert state is not None
1951 assert state.conflict_paths == ["config.py::B"]
1952 assert state.original_conflict_paths == ["config.py::A", "config.py::B"]
1953
1954 def test_original_paths_preserved_when_all_resolved(
1955 self, repo: pathlib.Path
1956 ) -> None:
1957 write_merge_state(
1958 repo,
1959 base_commit=long_id("0" * 64),
1960 ours_commit=long_id("1" * 64),
1961 theirs_commit=long_id("2" * 64),
1962 conflict_paths=["config.py::MAX_CONNECTIONS"],
1963 )
1964 write_merge_state(
1965 repo,
1966 base_commit=long_id("0" * 64),
1967 ours_commit=long_id("1" * 64),
1968 theirs_commit=long_id("2" * 64),
1969 conflict_paths=[],
1970 )
1971 state = read_merge_state(repo)
1972 assert state is not None
1973 assert state.conflict_paths == []
1974 assert state.original_conflict_paths == ["config.py::MAX_CONNECTIONS"]
1975
1976 def test_commit_pattern_uses_original_paths(self, repo: pathlib.Path) -> None:
1977 ours_id = _write_obj(repo, b"MAX_CONNECTIONS = 50")
1978 theirs_id = _write_obj(repo, b"MAX_CONNECTIONS = 25")
1979 resolution_id = _write_obj(repo, b"MAX_CONNECTIONS = 50")
1980
1981 write_merge_state(
1982 repo,
1983 base_commit=long_id("0" * 64),
1984 ours_commit=long_id("1" * 64),
1985 theirs_commit=long_id("2" * 64),
1986 conflict_paths=["config.py::MAX_CONNECTIONS"],
1987 )
1988 write_merge_state(
1989 repo,
1990 base_commit=long_id("0" * 64),
1991 ours_commit=long_id("1" * 64),
1992 theirs_commit=long_id("2" * 64),
1993 conflict_paths=[],
1994 )
1995 state = read_merge_state(repo)
1996 assert state is not None
1997
1998 paths_for_harmony = state.original_conflict_paths or state.conflict_paths
1999 saved = record_resolutions(
2000 repo, paths_for_harmony,
2001 {"config.py": ours_id},
2002 {"config.py": theirs_id},
2003 {"config.py": resolution_id},
2004 "code", _FakePlugin(),
2005 )
2006 assert saved == ["config.py::MAX_CONNECTIONS"]
2007
2008 def test_three_path_stepwise_resolution(self, repo: pathlib.Path) -> None:
2009 """Simulate resolving three conflicts one-by-one via checkout --ours."""
2010 original = ["a.py::X", "b.py::Y", "c.py::Z"]
2011 write_merge_state(
2012 repo,
2013 base_commit=long_id("0" * 64),
2014 ours_commit=long_id("1" * 64),
2015 theirs_commit=long_id("2" * 64),
2016 conflict_paths=original,
2017 )
2018 # Resolve X
2019 write_merge_state(
2020 repo,
2021 base_commit=long_id("0" * 64),
2022 ours_commit=long_id("1" * 64),
2023 theirs_commit=long_id("2" * 64),
2024 conflict_paths=["b.py::Y", "c.py::Z"],
2025 )
2026 # Resolve Y
2027 write_merge_state(
2028 repo,
2029 base_commit=long_id("0" * 64),
2030 ours_commit=long_id("1" * 64),
2031 theirs_commit=long_id("2" * 64),
2032 conflict_paths=["c.py::Z"],
2033 )
2034 # Resolve Z
2035 write_merge_state(
2036 repo,
2037 base_commit=long_id("0" * 64),
2038 ours_commit=long_id("1" * 64),
2039 theirs_commit=long_id("2" * 64),
2040 conflict_paths=[],
2041 )
2042 state = read_merge_state(repo)
2043 assert state is not None
2044 assert state.conflict_paths == []
2045 assert state.original_conflict_paths == sorted(original)
2046
2047
2048 # ===========================================================================
2049 # 26. Full end-to-end integration
2050 # ===========================================================================
2051
2052
2053 class TestEndToEndIntegration:
2054 """High-level workflows that exercise the full harmony lifecycle."""
2055
2056 def test_full_exact_replay_cycle(self, repo: pathlib.Path) -> None:
2057 """Conflict → record → same conflict → auto-apply → applied_count == 1."""
2058 ours_id = _write_obj(repo, b"MAX_CONNECTIONS = 10")
2059 theirs_id = _write_obj(repo, b"MAX_CONNECTIONS = 50")
2060 resolution_content = b"MAX_CONNECTIONS = 50"
2061 resolution_id = _write_obj(repo, resolution_content)
2062
2063 # Step 1: First conflict. auto_apply has nothing → records pattern.
2064 ours_m: Manifest = {"config.py": ours_id}
2065 theirs_m: Manifest = {"config.py": theirs_id}
2066 resolved, remaining = auto_apply(repo, ["config.py"], ours_m, theirs_m, "code", _FakePlugin())
2067 assert resolved == {}
2068 assert "config.py" in remaining
2069 assert len(list_patterns(repo)) == 1
2070
2071 # Step 2: User resolves and commits → record_resolutions learns the outcome.
2072 new_m: Manifest = {"config.py": resolution_id}
2073 saved = record_resolutions(repo, ["config.py"], ours_m, theirs_m, new_m, "code", _FakePlugin())
2074 assert saved == ["config.py"]
2075
2076 p = list_patterns(repo)[0]
2077 resolutions = list_resolutions(repo, p.pattern_id)
2078 assert len(resolutions) == 1
2079 assert resolutions[0].human_verified is True
2080
2081 # Step 3: Same conflict recurs → auto_apply replays.
2082 dest = repo / "config.py"
2083 resolved, remaining = auto_apply(repo, ["config.py"], ours_m, theirs_m, "code", _FakePlugin())
2084 assert "config.py" in resolved
2085 assert remaining == []
2086 assert dest.read_bytes() == resolution_content
2087
2088 # Step 4: applied_count has been incremented.
2089 resolutions = list_resolutions(repo, p.pattern_id)
2090 assert resolutions[0].applied_count == 1
2091
2092 def test_symbol_path_full_cycle(self, repo: pathlib.Path) -> None:
2093 """Full cycle with symbol-level conflict path."""
2094 ours_id = _write_obj(repo, b"TIMEOUT = 30")
2095 theirs_id = _write_obj(repo, b"TIMEOUT = 60")
2096 resolution_content = b"TIMEOUT = 45"
2097 resolution_id = _write_obj(repo, resolution_content)
2098
2099 ours_m: Manifest = {"settings.py": ours_id}
2100 theirs_m: Manifest = {"settings.py": theirs_id}
2101
2102 # First conflict — records pattern
2103 auto_apply(repo, ["settings.py::TIMEOUT"], ours_m, theirs_m, "code", _FakePlugin())
2104 assert len(list_patterns(repo)) == 1
2105
2106 # Record resolution
2107 record_resolutions(
2108 repo, ["settings.py::TIMEOUT"], ours_m, theirs_m,
2109 {"settings.py": resolution_id}, "code", _FakePlugin()
2110 )
2111
2112 # Replay
2113 dest = repo / "settings.py"
2114 resolved, remaining = auto_apply(
2115 repo, ["settings.py::TIMEOUT"], ours_m, theirs_m, "code", _FakePlugin()
2116 )
2117 assert "settings.py::TIMEOUT" in resolved
2118 assert remaining == []
2119 assert dest.read_bytes() == resolution_content
2120
2121 def test_gc_clears_stale_unlearned_patterns(self, repo: pathlib.Path) -> None:
2122 """Patterns auto_apply recorded (no resolution) are GC'd after threshold."""
2123 ours_id = _write_obj(repo, b"v1")
2124 theirs_id = _write_obj(repo, b"v2")
2125 ours_m: Manifest = {"f.py": ours_id}
2126 theirs_m: Manifest = {"f.py": theirs_id}
2127
2128 auto_apply(repo, ["f.py"], ours_m, theirs_m, "code", _FakePlugin())
2129 assert len(list_patterns(repo)) == 1
2130
2131 # Backdate the recorded_at
2132 from dataclasses import replace as dc_replace
2133 p = list_patterns(repo)[0]
2134 old_p = dc_replace(p, recorded_at=_now() - datetime.timedelta(days=100))
2135 forget_pattern(repo, p.pattern_id)
2136 record_pattern(repo, old_p)
2137
2138 removed = gc_stale(repo, age_days=90)
2139 assert removed == 1
2140 assert list_patterns(repo) == []
2141
2142 def test_policy_takes_precedence_in_match(self, repo: pathlib.Path) -> None:
2143 """Saved policy matches a pattern — match_policy returns the policy."""
2144 policy = _make_policy(
2145 "prefer-ours-for-config",
2146 action=PolicyAction.PREFER_OURS,
2147 path_pattern="config.py",
2148 )
2149 save_policy(repo, policy)
2150
2151 ours_id = _write_obj(repo, b"ours")
2152 theirs_id = _write_obj(repo, b"theirs")
2153 blob_fp = blob_fingerprint(ours_id, theirs_id)
2154 pid = compute_pattern_id("config.py", blob_fp, blob_fp)
2155 pattern = ConflictPattern(
2156 pattern_id=pid,
2157 path="config.py",
2158 domain="code",
2159 conflict_type=ConflictType.CONTENT,
2160 blob_fingerprint=blob_fp,
2161 semantic_fingerprint=blob_fp,
2162 ours_id=ours_id,
2163 theirs_id=theirs_id,
2164 description={},
2165 recorded_at=_now(),
2166 recorded_by="test",
2167 )
2168 policies = list_policies(repo)
2169 matched = match_policy(policies, pattern)
2170 assert matched is not None
2171 assert matched.policy_id == "prefer-ours-for-config"
2172 assert matched.action == PolicyAction.PREFER_OURS
2173
2174 def test_escalation_full_lifecycle(self, repo: pathlib.Path) -> None:
2175 """Open escalation → load → resolve → status becomes RESOLVED."""
2176 p = _make_pattern(repo)
2177 record_pattern(repo, p)
2178
2179 reason = "no policy and no prior resolution"
2180 esc_id = compute_escalation_id(p.pattern_id, reason)
2181 esc = EscalationRecord(
2182 escalation_id=esc_id,
2183 pattern_id=p.pattern_id,
2184 reason=reason,
2185 escalated_at=_now(),
2186 escalated_by=AgentProvenance.agent("claude-code"),
2187 status=EscalationStatus.OPEN,
2188 )
2189 record_escalation(repo, esc)
2190
2191 loaded = load_escalation(repo, esc_id)
2192 assert loaded is not None
2193 assert loaded.status == EscalationStatus.OPEN
2194
2195 # Human resolves
2196 r = _make_resolution(p, b"manual-outcome", repo)
2197 save_resolution(repo, r)
2198
2199 resolve_escalation(repo, esc_id, r.resolution_id, AgentProvenance.human(), _now())
2200
2201 loaded = load_escalation(repo, esc_id)
2202 assert loaded is not None
2203 assert loaded.status == EscalationStatus.RESOLVED
2204 assert loaded.resolution_id == r.resolution_id
2205
2206 open_escs = list_escalations(repo, status=EscalationStatus.OPEN)
2207 assert len(open_escs) == 0
2208
2209 def test_clear_all_then_replay_learns_fresh(self, repo: pathlib.Path) -> None:
2210 """After clear_all, harmony starts fresh — no stale resolutions replayed."""
2211 ours_id = _write_obj(repo, b"ours")
2212 theirs_id = _write_obj(repo, b"theirs")
2213 res_id = _write_obj(repo, b"res")
2214 ours_m: Manifest = {"f.py": ours_id}
2215 theirs_m: Manifest = {"f.py": theirs_id}
2216 new_m: Manifest = {"f.py": res_id}
2217
2218 record_resolutions(repo, ["f.py"], ours_m, theirs_m, new_m, "code", _FakePlugin())
2219 assert len(list_patterns(repo)) == 1
2220
2221 clear_all(repo)
2222 assert list_patterns(repo) == []
2223
2224 resolved, remaining = auto_apply(repo, ["f.py"], ours_m, theirs_m, "code", _FakePlugin())
2225 assert resolved == {}
2226 assert "f.py" in remaining
2227
2228 def test_multiple_resolutions_best_wins(self, repo: pathlib.Path) -> None:
2229 """When two resolutions exist, auto_apply picks the best."""
2230 ours_id = _write_obj(repo, b"ours")
2231 theirs_id = _write_obj(repo, b"theirs")
2232
2233 p = _make_pattern(repo, ours_content=b"ours", theirs_content=b"theirs")
2234 record_pattern(repo, p)
2235
2236 # Unverified low-confidence resolution
2237 r_low = _make_resolution(p, b"low-quality-outcome", repo,
2238 human_verified=False, confidence=0.4)
2239 save_resolution(repo, r_low)
2240
2241 # Human-verified high-confidence resolution
2242 high_content = b"high-quality-outcome"
2243 r_high = _make_resolution(p, high_content, repo,
2244 human_verified=True, confidence=1.0)
2245 save_resolution(repo, r_high)
2246
2247 dest = repo / "config.py"
2248 resolved, remaining = auto_apply(
2249 repo, ["config.py"],
2250 {"config.py": ours_id},
2251 {"config.py": theirs_id},
2252 "code", _FakePlugin(),
2253 )
2254 assert "config.py" in resolved
2255 assert dest.read_bytes() == high_content
2256
2257
2258 # ===========================================================================
2259 # 27. AgentProvenance serialization
2260 # ===========================================================================
2261
2262
2263 class TestAgentProvenance:
2264 def test_human_provenance(self) -> None:
2265 prov = AgentProvenance.human()
2266 assert prov.type == "human"
2267 assert prov.agent_id is None
2268 assert prov.model_id is None
2269 d = prov.to_dict()
2270 assert d["type"] == "human"
2271
2272 def test_agent_provenance(self) -> None:
2273 prov = AgentProvenance.agent("claude-code", "claude-sonnet-4-6")
2274 assert prov.type == "agent"
2275 assert prov.agent_id == "claude-code"
2276 assert prov.model_id == "claude-sonnet-4-6"
2277
2278 def test_round_trip(self) -> None:
2279 prov = AgentProvenance.agent("claude-code", "claude-sonnet-4-6")
2280 d = prov.to_dict()
2281 restored = AgentProvenance.from_dict(d)
2282 assert restored == prov
2283
2284 def test_from_dict_missing_fields_defaults(self) -> None:
2285 restored = AgentProvenance.from_dict({})
2286 assert restored.type == "human"
2287 assert restored.agent_id is None
2288 assert restored.model_id is None
2289
2290 def test_agent_without_model(self) -> None:
2291 prov = AgentProvenance.agent("my-agent")
2292 assert prov.model_id is None
2293 d = prov.to_dict()
2294 restored = AgentProvenance.from_dict(d)
2295 assert restored.agent_id == "my-agent"
2296 assert restored.model_id is None
2297
2298
2299 # ---------------------------------------------------------------------------
2300 # Content-addressed ID format — all harmony IDs must carry sha256: prefix
2301 # ---------------------------------------------------------------------------
2302
2303
2304 class TestHarmonyIdFormat:
2305 """All compute_*_id functions must return sha256:-prefixed IDs (length 71)."""
2306
2307 def _obj_id(self, seed: str) -> str:
2308 return blob_id(seed.encode())
2309
2310 # blob_fingerprint
2311
2312 def test_blob_fingerprint_sha256_prefix(self) -> None:
2313 fp = blob_fingerprint(self._obj_id("a"), self._obj_id("b"))
2314 assert fp.startswith("sha256:"), f"expected sha256: prefix, got {fp!r}"
2315 assert len(fp) == 71
2316
2317 def test_blob_fingerprint_commutative(self) -> None:
2318 a, b = self._obj_id("x"), self._obj_id("y")
2319 assert blob_fingerprint(a, b) == blob_fingerprint(b, a)
2320
2321 def test_blob_fingerprint_deterministic(self) -> None:
2322 a, b = self._obj_id("p"), self._obj_id("q")
2323 assert blob_fingerprint(a, b) == blob_fingerprint(a, b)
2324
2325 def test_blob_fingerprint_differs_by_inputs(self) -> None:
2326 a, b, c = self._obj_id("a"), self._obj_id("b"), self._obj_id("c")
2327 assert blob_fingerprint(a, b) != blob_fingerprint(a, c)
2328
2329 # compute_pattern_id
2330
2331 def test_compute_pattern_id_sha256_prefix(self, tmp_path: pathlib.Path) -> None:
2332 fp = blob_fingerprint(self._obj_id("a"), self._obj_id("b"))
2333 pid = compute_pattern_id("file.py", fp, fp)
2334 assert pid.startswith("sha256:")
2335 assert len(pid) == 71
2336
2337 def test_compute_pattern_id_deterministic(self) -> None:
2338 fp = blob_fingerprint(self._obj_id("a"), self._obj_id("b"))
2339 assert compute_pattern_id("f.py", fp, fp) == compute_pattern_id("f.py", fp, fp)
2340
2341 def test_compute_pattern_id_differs_by_path(self) -> None:
2342 fp = blob_fingerprint(self._obj_id("a"), self._obj_id("b"))
2343 assert compute_pattern_id("a.py", fp, fp) != compute_pattern_id("b.py", fp, fp)
2344
2345 def test_compute_pattern_id_semantic_differs_from_blob(self) -> None:
2346 blob_fp = blob_fingerprint(self._obj_id("a"), self._obj_id("b"))
2347 sem_fp = blob_fingerprint(self._obj_id("c"), self._obj_id("d"))
2348 pid_blob = compute_pattern_id("f.py", blob_fp, blob_fp)
2349 pid_sem = compute_pattern_id("f.py", blob_fp, sem_fp)
2350 assert pid_blob != pid_sem
2351
2352 # compute_resolution_id
2353
2354 def test_compute_resolution_id_sha256_prefix(self) -> None:
2355 fp = blob_fingerprint(self._obj_id("a"), self._obj_id("b"))
2356 pid = compute_pattern_id("f.py", fp, fp)
2357 rid = compute_resolution_id(
2358 pid,
2359 self._obj_id("resolved"),
2360 "ours",
2361 AgentProvenance.human(),
2362 datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc),
2363 )
2364 assert rid.startswith("sha256:")
2365 assert len(rid) == 71
2366
2367 def test_compute_resolution_id_deterministic(self) -> None:
2368 fp = blob_fingerprint(self._obj_id("a"), self._obj_id("b"))
2369 pid = compute_pattern_id("f.py", fp, fp)
2370 ts = datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc)
2371 r1 = compute_resolution_id(pid, self._obj_id("r"), "ours", AgentProvenance.human(), ts)
2372 r2 = compute_resolution_id(pid, self._obj_id("r"), "ours", AgentProvenance.human(), ts)
2373 assert r1 == r2
2374
2375 def test_compute_resolution_id_differs_by_outcome(self) -> None:
2376 fp = blob_fingerprint(self._obj_id("a"), self._obj_id("b"))
2377 pid = compute_pattern_id("f.py", fp, fp)
2378 ts = datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc)
2379 r1 = compute_resolution_id(pid, self._obj_id("r1"), "ours", AgentProvenance.human(), ts)
2380 r2 = compute_resolution_id(pid, self._obj_id("r2"), "ours", AgentProvenance.human(), ts)
2381 assert r1 != r2
2382
2383 # compute_escalation_id
2384
2385 def test_compute_escalation_id_sha256_prefix(self) -> None:
2386 fp = blob_fingerprint(self._obj_id("a"), self._obj_id("b"))
2387 pid = compute_pattern_id("f.py", fp, fp)
2388 eid = compute_escalation_id(pid, "no matching policy")
2389 assert eid.startswith("sha256:")
2390 assert len(eid) == 71
2391
2392 def test_compute_escalation_id_deterministic(self) -> None:
2393 fp = blob_fingerprint(self._obj_id("a"), self._obj_id("b"))
2394 pid = compute_pattern_id("f.py", fp, fp)
2395 e1 = compute_escalation_id(pid, "reason")
2396 e2 = compute_escalation_id(pid, "reason")
2397 assert e1 == e2
2398
2399 def test_compute_escalation_id_differs_by_reason(self) -> None:
2400 fp = blob_fingerprint(self._obj_id("a"), self._obj_id("b"))
2401 pid = compute_pattern_id("f.py", fp, fp)
2402 e1 = compute_escalation_id(pid, "reason A")
2403 e2 = compute_escalation_id(pid, "reason B")
2404 assert e1 != e2
2405
2406 # audit_id
2407
2408 def test_append_audit_id_sha256_prefix(self, tmp_path: pathlib.Path) -> None:
2409 fp = blob_fingerprint(self._obj_id("a"), self._obj_id("b"))
2410 pid = compute_pattern_id("f.py", fp, fp)
2411 append_audit(
2412 tmp_path,
2413 AuditEventType.PATTERN_RECORDED,
2414 AgentProvenance.human(),
2415 pattern_id=pid,
2416 )
2417 entries = list_audit(tmp_path, limit=1)
2418 assert len(entries) == 1
2419 assert entries[0]["audit_id"].startswith("sha256:")
2420 assert len(entries[0]["audit_id"]) == 71
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