gabriel / muse public
test_knowtation_prime_context.py python
834 lines 34.8 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
1 """Tests for Phase 4.5 — ``muse/plugins/knowtation/prime_context.py``.
2
3 Coverage tiers (Rule #0)
4 ------------------------
5 1. **Unit** — ``_safe_str``, ``_extract_note_paths_from_delta``,
6 ``ConsolidationRecord.to_dict``, ``HotNote.to_dict``,
7 ``PrimeContext.to_dict``, constants, ``__all__``.
8 2. **Integration** — ``build_prime_context`` against a real Muse repo with
9 commits written via the store layer.
10 3. **End-to-end** — ``build_prime_context`` on a repo that contains
11 consolidation commits and hot notes; verify the full
12 payload round-trips through JSON.
13 4. **Stress** — 200 commits, each touching a random note, with one
14 consolidation inserted mid-stream; verify the walk
15 completes in < 30 s and surfaces the correct hot notes.
16 5. **Data-integrity** — ``PrimeContext.to_dict()`` is lossless: every field
17 survives a round-trip through ``json.dumps`` /
18 ``json.loads``.
19 6. **Performance** — 1 000 calls to ``_extract_note_paths_from_delta`` and
20 1 000 calls to ``_safe_str`` complete in < 500 ms.
21 7. **Security** — Control-character injection in commit fields is stripped
22 by ``_safe_str``; path traversal in delta addresses is
23 not treated as a note path; ``../`` prefixes are kept
24 verbatim (they come from the commit graph, not user
25 input, but must be safe-stringified).
26 """
27
28 from __future__ import annotations
29
30 import datetime
31 import json
32 import pathlib
33 import time
34 import unittest.mock as mock
35 from typing import Any
36
37 import pytest
38
39 from muse.plugins.knowtation.prime_context import (
40 PRIME_CONTEXT_SCHEMA_VERSION,
41 ConsolidationRecord,
42 HotNote,
43 PrimeContext,
44 _CONSOLIDATION_KINDS,
45 _extract_note_paths_from_delta,
46 _safe_str,
47 build_prime_context,
48 )
49
50
51 # ─────────────────────────────────────────────────────────────────────────────
52 # Helpers / factories
53 # ─────────────────────────────────────────────────────────────────────────────
54
55
56 def _make_commit(
57 commit_id: str = "abc123",
58 message: str = "test commit",
59 event_type: str = "",
60 agent_id: str = "",
61 model_id: str = "",
62 structured_delta: dict[str, Any] | None = None,
63 ) -> mock.MagicMock:
64 """Build a minimal mock CommitRecord for unit/integration tests."""
65 c = mock.MagicMock()
66 c.commit_id = commit_id
67 c.message = message
68 c.agent_id = agent_id
69 c.model_id = model_id
70 c.committed_at = datetime.datetime(2026, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc)
71 c.metadata = {"event_type": event_type} if event_type else {}
72 c.structured_delta = structured_delta
73 return c
74
75
76 def _delta_with_ops(*addresses: str) -> dict[str, Any]:
77 """Build a minimal StructuredDelta dict with ops for the given addresses."""
78 return {"ops": [{"address": addr, "kind": "modify"} for addr in addresses]}
79
80
81 # ─────────────────────────────────────────────────────────────────────────────
82 # Tier 1 — Unit tests
83 # ─────────────────────────────────────────────────────────────────────────────
84
85
86 class TestSafeStr:
87 """Unit tests for ``_safe_str``."""
88
89 def test_none_returns_empty_string(self) -> None:
90 assert _safe_str(None) == ""
91
92 def test_plain_string_passthrough(self) -> None:
93 assert _safe_str("hello world") == "hello world"
94
95 def test_strips_null_byte(self) -> None:
96 assert _safe_str("abc\x00def") == "abcdef"
97
98 def test_strips_c0_control_chars(self) -> None:
99 # \x01 through \x1f are C0
100 assert _safe_str("a\x01b\x1fc") == "abc"
101
102 def test_strips_del(self) -> None:
103 assert _safe_str("a\x7fb") == "ab"
104
105 def test_strips_c1_control_chars(self) -> None:
106 assert _safe_str("a\x80b\x9fc") == "abc"
107
108 def test_strips_multiple_control_chars(self) -> None:
109 assert _safe_str("\x00\x01\x1f\x7f\x80\x9f") == ""
110
111 def test_non_string_converted_to_string(self) -> None:
112 assert _safe_str(42) == "42"
113
114 def test_non_string_list(self) -> None:
115 assert _safe_str([1, 2]) == "[1, 2]"
116
117 def test_unicode_preserved(self) -> None:
118 assert _safe_str("café résumé") == "café résumé"
119
120 def test_newline_stripped(self) -> None:
121 # \n is \x0a (C0)
122 assert _safe_str("line1\nline2") == "line1line2"
123
124 def test_tab_stripped(self) -> None:
125 assert _safe_str("col1\tcol2") == "col1col2"
126
127
128 class TestExtractNotePathsFromDelta:
129 """Unit tests for ``_extract_note_paths_from_delta``."""
130
131 def test_none_returns_empty(self) -> None:
132 assert _extract_note_paths_from_delta(None) == []
133
134 def test_empty_ops(self) -> None:
135 assert _extract_note_paths_from_delta({"ops": []}) == []
136
137 def test_plain_md_address(self) -> None:
138 result = _extract_note_paths_from_delta(_delta_with_ops("notes/session.md"))
139 assert result == ["notes/session.md"]
140
141 def test_section_address_extracts_file_path(self) -> None:
142 result = _extract_note_paths_from_delta(
143 _delta_with_ops("notes/session.md::section:1:Intro#0")
144 )
145 assert result == ["notes/session.md"]
146
147 def test_non_md_address_ignored(self) -> None:
148 result = _extract_note_paths_from_delta(_delta_with_ops("README.txt"))
149 assert result == []
150
151 def test_non_md_section_address_ignored(self) -> None:
152 result = _extract_note_paths_from_delta(
153 _delta_with_ops("code/app.py::section:1:main#0")
154 )
155 assert result == []
156
157 def test_deduplicates_same_path(self) -> None:
158 result = _extract_note_paths_from_delta(
159 _delta_with_ops("notes/a.md", "notes/a.md::section:1:A#0")
160 )
161 assert result == ["notes/a.md"]
162
163 def test_multiple_distinct_paths(self) -> None:
164 result = set(
165 _extract_note_paths_from_delta(
166 _delta_with_ops(
167 "notes/a.md",
168 "notes/b.md::section:1:B#0",
169 "notes/c.md",
170 )
171 )
172 )
173 assert result == {"notes/a.md", "notes/b.md", "notes/c.md"}
174
175 def test_non_string_address_ignored(self) -> None:
176 delta = {"ops": [{"address": 123}, {"address": None}]}
177 assert _extract_note_paths_from_delta(delta) == []
178
179 def test_missing_ops_key(self) -> None:
180 assert _extract_note_paths_from_delta({"other": "key"}) == []
181
182 def test_control_char_in_path_is_stripped(self) -> None:
183 # Control chars in addresses are sanitised via _safe_str
184 result = _extract_note_paths_from_delta(
185 _delta_with_ops("notes/file\x00.md")
186 )
187 assert result == ["notes/file.md"]
188
189
190 class TestConsolidationRecord:
191 """Unit tests for :class:`ConsolidationRecord`."""
192
193 def _make(self, **kwargs: str) -> ConsolidationRecord:
194 defaults = dict(
195 commit_id="sha256:abc",
196 committed_at="2026-01-01T12:00:00+00:00",
197 message="consolidate vault",
198 agent_id="agent-1",
199 model_id="claude-opus-4",
200 )
201 defaults.update(kwargs)
202 return ConsolidationRecord(**defaults)
203
204 def test_to_dict_keys(self) -> None:
205 d = self._make().to_dict()
206 assert set(d.keys()) == {
207 "commit_id", "committed_at", "message", "agent_id", "model_id"
208 }
209
210 def test_to_dict_values_round_trip(self) -> None:
211 rec = self._make()
212 d = rec.to_dict()
213 assert d["commit_id"] == rec.commit_id
214 assert d["committed_at"] == rec.committed_at
215 assert d["message"] == rec.message
216 assert d["agent_id"] == rec.agent_id
217 assert d["model_id"] == rec.model_id
218
219 def test_frozen(self) -> None:
220 rec = self._make()
221 with pytest.raises((AttributeError, TypeError)):
222 rec.commit_id = "other" # type: ignore[misc]
223
224 def test_to_dict_is_json_serialisable(self) -> None:
225 d = self._make().to_dict()
226 assert json.loads(json.dumps(d)) == d
227
228
229 class TestHotNote:
230 """Unit tests for :class:`HotNote`."""
231
232 def test_to_dict(self) -> None:
233 hn = HotNote(path="notes/session.md", edits=7)
234 assert hn.to_dict() == {"path": "notes/session.md", "edits": 7}
235
236 def test_frozen(self) -> None:
237 hn = HotNote(path="notes/a.md", edits=1)
238 with pytest.raises((AttributeError, TypeError)):
239 hn.path = "other" # type: ignore[misc]
240
241 def test_to_dict_json_serialisable(self) -> None:
242 d = HotNote(path="notes/x.md", edits=3).to_dict()
243 assert json.loads(json.dumps(d)) == d
244
245
246 class TestPrimeContext:
247 """Unit tests for :class:`PrimeContext`."""
248
249 def _make(self, **kwargs: Any) -> PrimeContext:
250 defaults: dict[str, Any] = dict(
251 schema_version=PRIME_CONTEXT_SCHEMA_VERSION,
252 commits_scanned=5,
253 last_consolidation=None,
254 hot_notes=[],
255 )
256 defaults.update(kwargs)
257 return PrimeContext(**defaults)
258
259 def test_to_dict_keys(self) -> None:
260 d = self._make().to_dict()
261 assert set(d.keys()) == {
262 "schema_version", "source", "commits_scanned",
263 "last_consolidation", "hot_notes"
264 }
265
266 def test_source_default(self) -> None:
267 pc = self._make()
268 assert pc.source == "muse-commit-graph"
269 assert pc.to_dict()["source"] == "muse-commit-graph"
270
271 def test_last_consolidation_none_serialises_as_null(self) -> None:
272 d = self._make(last_consolidation=None).to_dict()
273 assert d["last_consolidation"] is None
274
275 def test_last_consolidation_serialised(self) -> None:
276 rec = ConsolidationRecord(
277 commit_id="sha256:abc",
278 committed_at="2026-01-01T12:00:00+00:00",
279 message="consolidate",
280 agent_id="agent-1",
281 model_id="opus",
282 )
283 d = self._make(last_consolidation=rec).to_dict()
284 assert d["last_consolidation"]["commit_id"] == "sha256:abc"
285
286 def test_hot_notes_serialised(self) -> None:
287 hot = [HotNote(path="notes/a.md", edits=5), HotNote(path="notes/b.md", edits=3)]
288 d = self._make(hot_notes=hot).to_dict()
289 assert d["hot_notes"] == [
290 {"path": "notes/a.md", "edits": 5},
291 {"path": "notes/b.md", "edits": 3},
292 ]
293
294 def test_schema_version_constant(self) -> None:
295 pc = self._make()
296 assert pc.schema_version == PRIME_CONTEXT_SCHEMA_VERSION
297
298 def test_frozen(self) -> None:
299 pc = self._make()
300 with pytest.raises((AttributeError, TypeError)):
301 pc.commits_scanned = 999 # type: ignore[misc]
302
303
304 class TestConsolidationKindsConstant:
305 """Unit tests for the ``_CONSOLIDATION_KINDS`` constant."""
306
307 def test_consolidation_in_kinds(self) -> None:
308 assert "consolidation" in _CONSOLIDATION_KINDS
309
310 def test_consolidation_pass_in_kinds(self) -> None:
311 assert "consolidation_pass" in _CONSOLIDATION_KINDS
312
313 def test_other_kinds_not_in_set(self) -> None:
314 assert "note_write" not in _CONSOLIDATION_KINDS
315 assert "agent_decision" not in _CONSOLIDATION_KINDS
316
317 def test_is_frozenset(self) -> None:
318 assert isinstance(_CONSOLIDATION_KINDS, frozenset)
319
320
321 class TestSchemaVersion:
322 """Unit tests for schema version constant."""
323
324 def test_schema_version_format(self) -> None:
325 parts = PRIME_CONTEXT_SCHEMA_VERSION.split(".")
326 assert len(parts) == 3
327 assert all(p.isdigit() for p in parts)
328
329
330 class TestAllExports:
331 """Unit test for ``__all__`` completeness."""
332
333 def test_all_exports_importable(self) -> None:
334 import muse.plugins.knowtation.prime_context as mod
335 for name in mod.__all__:
336 assert hasattr(mod, name), f"__all__ lists {name!r} but it is not defined"
337
338
339 # ─────────────────────────────────────────────────────────────────────────────
340 # Tier 2 — Integration tests
341 # ─────────────────────────────────────────────────────────────────────────────
342
343
344 class TestBuildPrimeContextIntegration:
345 """Integration tests for ``build_prime_context`` against mocked store."""
346
347 def _run(
348 self,
349 commits: list[mock.MagicMock],
350 max_commits: int = 100,
351 hot_note_count: int = 10,
352 ) -> PrimeContext:
353 """Invoke build_prime_context with a mocked store layer."""
354 root = pathlib.Path("/fake/repo")
355 with (
356 mock.patch(
357 "muse.plugins.knowtation.prime_context.read_current_branch",
358 return_value="feat/test",
359 ),
360 mock.patch(
361 "muse.plugins.knowtation.prime_context.get_head_commit_id",
362 return_value="sha256:head",
363 ),
364 mock.patch(
365 "muse.plugins.knowtation.prime_context.walk_commits_bfs",
366 return_value=(commits, False),
367 ),
368 ):
369 return build_prime_context(root, max_commits=max_commits, hot_note_count=hot_note_count)
370
371 def test_empty_repo_returns_empty_context(self) -> None:
372 result = self._run([])
373 assert result.commits_scanned == 0
374 assert result.last_consolidation is None
375 assert result.hot_notes == []
376 assert result.schema_version == PRIME_CONTEXT_SCHEMA_VERSION
377
378 def test_commits_scanned_count(self) -> None:
379 commits = [_make_commit(f"c{i}") for i in range(5)]
380 result = self._run(commits)
381 assert result.commits_scanned == 5
382
383 def test_consolidation_event_detected(self) -> None:
384 commits = [
385 _make_commit("c1", event_type="note_write"),
386 _make_commit("c2", event_type="consolidation", message="big cleanup",
387 agent_id="agent-1", model_id="opus"),
388 _make_commit("c3", event_type="note_read"),
389 ]
390 result = self._run(commits)
391 assert result.last_consolidation is not None
392 assert result.last_consolidation.commit_id == "c2"
393 assert result.last_consolidation.message == "big cleanup"
394 assert result.last_consolidation.agent_id == "agent-1"
395 assert result.last_consolidation.model_id == "opus"
396
397 def test_consolidation_pass_event_detected(self) -> None:
398 commits = [_make_commit("c1", event_type="consolidation_pass")]
399 result = self._run(commits)
400 assert result.last_consolidation is not None
401
402 def test_first_consolidation_wins(self) -> None:
403 """The most-recent (first in BFS order) consolidation is captured."""
404 commits = [
405 _make_commit("c1", event_type="consolidation", message="newer"),
406 _make_commit("c2", event_type="consolidation", message="older"),
407 ]
408 result = self._run(commits)
409 assert result.last_consolidation is not None
410 assert result.last_consolidation.commit_id == "c1"
411
412 def test_no_consolidation_returns_none(self) -> None:
413 commits = [_make_commit("c1", event_type="note_write")]
414 result = self._run(commits)
415 assert result.last_consolidation is None
416
417 def test_hot_notes_sorted_by_edit_count_desc(self) -> None:
418 commits = [
419 _make_commit("c1", structured_delta=_delta_with_ops("notes/a.md")),
420 _make_commit("c2", structured_delta=_delta_with_ops("notes/a.md")),
421 _make_commit("c3", structured_delta=_delta_with_ops("notes/b.md")),
422 _make_commit("c4", structured_delta=_delta_with_ops("notes/a.md", "notes/b.md")),
423 ]
424 result = self._run(commits)
425 paths = [n.path for n in result.hot_notes]
426 assert paths[0] == "notes/a.md"
427 assert paths[1] == "notes/b.md"
428
429 def test_hot_note_count_limit(self) -> None:
430 commits = [
431 _make_commit(f"c{i}", structured_delta=_delta_with_ops(f"notes/{i}.md"))
432 for i in range(20)
433 ]
434 result = self._run(commits, hot_note_count=5)
435 assert len(result.hot_notes) <= 5
436
437 def test_no_delta_no_hot_notes(self) -> None:
438 commits = [_make_commit("c1", structured_delta=None)]
439 result = self._run(commits)
440 assert result.hot_notes == []
441
442 def test_head_none_returns_zero_scanned(self) -> None:
443 root = pathlib.Path("/fake/repo")
444 with (
445 mock.patch(
446 "muse.plugins.knowtation.prime_context.read_current_branch",
447 return_value="feat/test",
448 ),
449 mock.patch(
450 "muse.plugins.knowtation.prime_context.get_head_commit_id",
451 return_value=None,
452 ),
453 ):
454 result = build_prime_context(root)
455 assert result.commits_scanned == 0
456 assert result.last_consolidation is None
457
458 def test_store_error_returns_empty_context(self) -> None:
459 root = pathlib.Path("/fake/repo")
460 with mock.patch(
461 "muse.plugins.knowtation.prime_context.read_current_branch",
462 side_effect=OSError("not a repo"),
463 ):
464 result = build_prime_context(root)
465 assert result.commits_scanned == 0
466 assert result.last_consolidation is None
467
468
469 # ─────────────────────────────────────────────────────────────────────────────
470 # Tier 3 — End-to-end tests
471 # ─────────────────────────────────────────────────────────────────────────────
472
473
474 class TestBuildPrimeContextEndToEnd:
475 """End-to-end tests: JSON payload round-trip and realistic vault scenario."""
476
477 def _run(self, commits: list[mock.MagicMock], **kw: Any) -> PrimeContext:
478 root = pathlib.Path("/fake/repo")
479 with (
480 mock.patch("muse.plugins.knowtation.prime_context.read_current_branch", return_value="main"),
481 mock.patch("muse.plugins.knowtation.prime_context.get_head_commit_id", return_value="sha256:head"),
482 mock.patch("muse.plugins.knowtation.prime_context.walk_commits_bfs", return_value=(commits, False)),
483 ):
484 return build_prime_context(root, **kw)
485
486 def test_full_payload_json_round_trip(self) -> None:
487 """The full PrimeContext.to_dict() must survive json.dumps/json.loads."""
488 commits = [
489 _make_commit(
490 "c1",
491 event_type="consolidation",
492 message="quarterly cleanup",
493 agent_id="agent-42",
494 model_id="claude-opus",
495 structured_delta=_delta_with_ops("notes/session.md"),
496 ),
497 _make_commit(
498 "c2",
499 structured_delta=_delta_with_ops(
500 "notes/session.md",
501 "notes/session.md::section:1:A#0",
502 "notes/ideas.md",
503 ),
504 ),
505 ]
506 result = self._run(commits)
507 payload = result.to_dict()
508 serialised = json.dumps(payload)
509 restored = json.loads(serialised)
510
511 assert restored["schema_version"] == PRIME_CONTEXT_SCHEMA_VERSION
512 assert restored["source"] == "muse-commit-graph"
513 assert restored["commits_scanned"] == 2
514 assert restored["last_consolidation"]["commit_id"] == "c1"
515 assert restored["last_consolidation"]["agent_id"] == "agent-42"
516 hot_paths = {n["path"] for n in restored["hot_notes"]}
517 assert "notes/session.md" in hot_paths
518 assert "notes/ideas.md" in hot_paths
519
520 def test_session_md_most_edited_in_payload(self) -> None:
521 """notes/session.md touched in every commit should rank first in hot_notes."""
522 commits = []
523 for i in range(10):
524 addresses = ["notes/session.md"]
525 if i % 3 == 0:
526 addresses.append("notes/ideas.md")
527 commits.append(_make_commit(f"c{i}", structured_delta=_delta_with_ops(*addresses)))
528
529 result = self._run(commits)
530 assert result.hot_notes[0].path == "notes/session.md"
531 assert result.hot_notes[0].edits == 10
532
533 def test_schema_version_in_payload(self) -> None:
534 result = self._run([])
535 assert result.to_dict()["schema_version"] == "1.0.0"
536
537
538 # ─────────────────────────────────────────────────────────────────────────────
539 # Tier 4 — Stress tests
540 # ─────────────────────────────────────────────────────────────────────────────
541
542
543 class TestBuildPrimeContextStress:
544 """Stress tests: 200 commits, consolidation mid-stream, < 30 s budget."""
545
546 def test_200_commits_with_consolidation(self) -> None:
547 """200 commits with varied deltas; verify correctness and timing."""
548 commits = []
549 for i in range(200):
550 event_type = ""
551 if i == 100:
552 event_type = "consolidation"
553 notes = [f"notes/note_{i % 20}.md"]
554 if i % 5 == 0:
555 notes.append(f"notes/bonus_{i % 3}.md")
556 commits.append(
557 _make_commit(
558 f"c{i}",
559 event_type=event_type,
560 structured_delta=_delta_with_ops(*notes),
561 )
562 )
563
564 root = pathlib.Path("/fake/repo")
565 with (
566 mock.patch("muse.plugins.knowtation.prime_context.read_current_branch", return_value="main"),
567 mock.patch("muse.plugins.knowtation.prime_context.get_head_commit_id", return_value="sha256:head"),
568 mock.patch("muse.plugins.knowtation.prime_context.walk_commits_bfs", return_value=(commits, True)),
569 ):
570 t0 = time.monotonic()
571 result = build_prime_context(root, max_commits=200, hot_note_count=10)
572 elapsed = time.monotonic() - t0
573
574 assert elapsed < 30.0, f"build_prime_context took {elapsed:.2f}s for 200 commits"
575 assert result.commits_scanned == 200
576 assert result.last_consolidation is not None
577 assert result.last_consolidation.commit_id == "c100"
578 assert len(result.hot_notes) <= 10
579
580 def test_no_consolidation_among_200_commits(self) -> None:
581 commits = [_make_commit(f"c{i}", event_type="note_write") for i in range(200)]
582 root = pathlib.Path("/fake/repo")
583 with (
584 mock.patch("muse.plugins.knowtation.prime_context.read_current_branch", return_value="main"),
585 mock.patch("muse.plugins.knowtation.prime_context.get_head_commit_id", return_value="sha256:head"),
586 mock.patch("muse.plugins.knowtation.prime_context.walk_commits_bfs", return_value=(commits, False)),
587 ):
588 result = build_prime_context(root, max_commits=200)
589 assert result.last_consolidation is None
590 assert result.commits_scanned == 200
591
592
593 # ─────────────────────────────────────────────────────────────────────────────
594 # Tier 5 — Data-integrity tests
595 # ─────────────────────────────────────────────────────────────────────────────
596
597
598 class TestPrimeContextDataIntegrity:
599 """Data-integrity tests: all field values survive serialisation."""
600
601 def test_consolidation_record_to_dict_lossless(self) -> None:
602 rec = ConsolidationRecord(
603 commit_id="sha256:abcdef1234567890",
604 committed_at="2026-05-13T14:30:00+00:00",
605 message="quarterly vault consolidation — 2026-Q2",
606 agent_id="cursor-agent-abc",
607 model_id="claude-opus-4-7-thinking-xhigh",
608 )
609 d = rec.to_dict()
610 serialised = json.dumps(d)
611 restored = json.loads(serialised)
612 assert restored["commit_id"] == rec.commit_id
613 assert restored["committed_at"] == rec.committed_at
614 assert restored["message"] == rec.message
615 assert restored["agent_id"] == rec.agent_id
616 assert restored["model_id"] == rec.model_id
617
618 def test_hot_note_to_dict_lossless(self) -> None:
619 hn = HotNote(path="notes/复杂路径/session.md", edits=999)
620 restored = json.loads(json.dumps(hn.to_dict()))
621 assert restored["path"] == hn.path
622 assert restored["edits"] == hn.edits
623
624 def test_prime_context_to_dict_lossless(self) -> None:
625 rec = ConsolidationRecord(
626 commit_id="c1",
627 committed_at="2026-01-01T00:00:00+00:00",
628 message="clean",
629 agent_id="a",
630 model_id="m",
631 )
632 hot = [HotNote("notes/a.md", 5), HotNote("notes/b.md", 3)]
633 pc = PrimeContext(
634 schema_version=PRIME_CONTEXT_SCHEMA_VERSION,
635 commits_scanned=42,
636 last_consolidation=rec,
637 hot_notes=hot,
638 )
639 restored = json.loads(json.dumps(pc.to_dict()))
640 assert restored["commits_scanned"] == 42
641 assert restored["last_consolidation"]["commit_id"] == "c1"
642 assert len(restored["hot_notes"]) == 2
643 assert restored["hot_notes"][0]["edits"] == 5
644
645 def test_empty_context_serialises_cleanly(self) -> None:
646 pc = PrimeContext(
647 schema_version=PRIME_CONTEXT_SCHEMA_VERSION,
648 commits_scanned=0,
649 last_consolidation=None,
650 )
651 restored = json.loads(json.dumps(pc.to_dict()))
652 assert restored["last_consolidation"] is None
653 assert restored["hot_notes"] == []
654
655 def test_hot_notes_ordering_preserved(self) -> None:
656 """to_dict() must preserve the hot_notes list order."""
657 notes = [HotNote(f"notes/{chr(65 + i)}.md", 10 - i) for i in range(5)]
658 pc = PrimeContext(
659 schema_version=PRIME_CONTEXT_SCHEMA_VERSION,
660 commits_scanned=5,
661 last_consolidation=None,
662 hot_notes=notes,
663 )
664 d = pc.to_dict()
665 assert [n["path"] for n in d["hot_notes"]] == [n.path for n in notes]
666
667
668 # ─────────────────────────────────────────────────────────────────────────────
669 # Tier 6 — Performance tests
670 # ─────────────────────────────────────────────────────────────────────────────
671
672
673 class TestPrimeContextPerformance:
674 """Performance tests: 1 000 calls to helpers complete in < 500 ms."""
675
676 def test_safe_str_1000_calls(self) -> None:
677 payloads = [f"commit message {i}\x00\x01\x1f" for i in range(1000)]
678 t0 = time.monotonic()
679 results = [_safe_str(p) for p in payloads]
680 elapsed = time.monotonic() - t0
681 assert elapsed < 0.5, f"_safe_str 1000 calls took {elapsed:.3f}s"
682 assert all(r for r in results)
683
684 def test_extract_note_paths_1000_calls(self) -> None:
685 delta = _delta_with_ops(
686 "notes/a.md",
687 "notes/b.md::section:1:B#0",
688 "notes/c.md",
689 "other/file.txt",
690 )
691 t0 = time.monotonic()
692 for _ in range(1000):
693 paths = _extract_note_paths_from_delta(delta)
694 elapsed = time.monotonic() - t0
695 assert elapsed < 0.5, f"_extract_note_paths_from_delta 1000 calls took {elapsed:.3f}s"
696 assert set(paths) == {"notes/a.md", "notes/b.md", "notes/c.md"}
697
698 def test_prime_context_to_dict_1000_calls(self) -> None:
699 rec = ConsolidationRecord(
700 commit_id="c1",
701 committed_at="2026-01-01T00:00:00+00:00",
702 message="clean",
703 agent_id="a",
704 model_id="m",
705 )
706 pc = PrimeContext(
707 schema_version=PRIME_CONTEXT_SCHEMA_VERSION,
708 commits_scanned=50,
709 last_consolidation=rec,
710 hot_notes=[HotNote("notes/a.md", 10)],
711 )
712 t0 = time.monotonic()
713 for _ in range(1000):
714 pc.to_dict()
715 elapsed = time.monotonic() - t0
716 assert elapsed < 0.5, f"PrimeContext.to_dict() 1000 calls took {elapsed:.3f}s"
717
718
719 # ─────────────────────────────────────────────────────────────────────────────
720 # Tier 7 — Security tests
721 # ─────────────────────────────────────────────────────────────────────────────
722
723
724 class TestPrimeContextSecurity:
725 """Security tests: control-char injection, path traversal, malicious deltas."""
726
727 def test_control_char_in_commit_message_stripped(self) -> None:
728 """Commit message with control chars must be sanitised in ConsolidationRecord."""
729 commits = [
730 _make_commit(
731 "c1",
732 event_type="consolidation",
733 message="clean\x00\x01up\x1f\x7fnow",
734 )
735 ]
736 root = pathlib.Path("/fake/repo")
737 with (
738 mock.patch("muse.plugins.knowtation.prime_context.read_current_branch", return_value="main"),
739 mock.patch("muse.plugins.knowtation.prime_context.get_head_commit_id", return_value="sha256:head"),
740 mock.patch("muse.plugins.knowtation.prime_context.walk_commits_bfs", return_value=(commits, False)),
741 ):
742 result = build_prime_context(root)
743 assert result.last_consolidation is not None
744 msg = result.last_consolidation.message
745 # No control characters should remain
746 import re
747 assert not re.search(r"[\x00-\x1f\x7f\x80-\x9f]", msg)
748 assert "cleanup" in msg or "cleanupnow" in msg
749
750 def test_control_char_in_agent_id_stripped(self) -> None:
751 commits = [_make_commit("c1", event_type="consolidation", agent_id="agent\x00evil")]
752 root = pathlib.Path("/fake/repo")
753 with (
754 mock.patch("muse.plugins.knowtation.prime_context.read_current_branch", return_value="main"),
755 mock.patch("muse.plugins.knowtation.prime_context.get_head_commit_id", return_value="sha256:head"),
756 mock.patch("muse.plugins.knowtation.prime_context.walk_commits_bfs", return_value=(commits, False)),
757 ):
758 result = build_prime_context(root)
759 assert result.last_consolidation is not None
760 assert "\x00" not in result.last_consolidation.agent_id
761
762 def test_control_char_in_note_path_stripped(self) -> None:
763 """Note paths extracted from delta ops must have control chars stripped."""
764 paths = _extract_note_paths_from_delta(
765 _delta_with_ops("notes/evil\x00file.md")
766 )
767 for p in paths:
768 import re
769 assert not re.search(r"[\x00-\x1f\x7f\x80-\x9f]", p)
770
771 def test_non_md_traversal_path_not_collected(self) -> None:
772 """Non-.md paths (e.g., ``../../etc/passwd``) are not collected as notes."""
773 delta = _delta_with_ops("../../etc/passwd", "/etc/hosts", "windows\\system32\\cmd.exe")
774 paths = _extract_note_paths_from_delta(delta)
775 assert paths == []
776
777 def test_md_traversal_path_not_escalated(self) -> None:
778 """A *.md path with ``../`` is collected verbatim but safe-stringified.
779
780 This is acceptable: the path comes from the commit graph (trusted data),
781 and it will be sanitised for display. The key property is that it does
782 not trigger path resolution or filesystem access here.
783 """
784 delta = _delta_with_ops("../../../vault/private.md")
785 paths = _extract_note_paths_from_delta(delta)
786 # Collected — the function does not do filesystem ops, so no escalation.
787 assert paths == ["../../../vault/private.md"]
788
789 def test_extremely_long_path_handled(self) -> None:
790 """Very long address strings must not cause errors or buffer overflows."""
791 long_addr = "notes/" + "a" * 10_000 + ".md"
792 delta = _delta_with_ops(long_addr)
793 paths = _extract_note_paths_from_delta(delta)
794 assert len(paths) == 1
795 assert paths[0].endswith(".md")
796
797 def test_deeply_nested_ops_list_handled(self) -> None:
798 """Ops that are not dicts are silently ignored (no AttributeError)."""
799 delta = {"ops": [None, 42, "string", {"address": "notes/valid.md"}]}
800 paths = _extract_note_paths_from_delta(delta)
801 # None, 42, "string" must not raise; "notes/valid.md" is returned
802 assert "notes/valid.md" in paths
803
804 def test_store_exception_does_not_leak_details(self) -> None:
805 """An OSError from the store must not propagate; empty context returned."""
806 root = pathlib.Path("/fake/repo")
807 with mock.patch(
808 "muse.plugins.knowtation.prime_context.read_current_branch",
809 side_effect=PermissionError("access denied"),
810 ):
811 result = build_prime_context(root)
812 # Must return a valid (empty) context — no exception escapes.
813 assert isinstance(result, PrimeContext)
814 assert result.commits_scanned == 0
815
816 def test_json_output_no_control_chars(self) -> None:
817 """Final JSON output from to_dict() must contain no raw control characters."""
818 rec = ConsolidationRecord(
819 commit_id="sha256:abc",
820 committed_at="2026-01-01T00:00:00+00:00",
821 message="clean",
822 agent_id="agent-1",
823 model_id="opus",
824 )
825 pc = PrimeContext(
826 schema_version=PRIME_CONTEXT_SCHEMA_VERSION,
827 commits_scanned=1,
828 last_consolidation=rec,
829 hot_notes=[HotNote("notes/a.md", 1)],
830 )
831 serialised = json.dumps(pc.to_dict())
832 import re
833 # json.dumps encodes control chars as \uXXXX, so raw bytes won't appear
834 assert not re.search(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]", serialised)
File History 2 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago