test_knowtation_compare.py
python
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6
chore(tests): add docstring to tests/__init__.py so rc14 tr…
Human
26 days ago
| 1 | """Tests for Phase 2.6 — Knowtation ``muse code compare`` JSON output. |
| 2 | |
| 3 | Test tiers covered |
| 4 | ------------------ |
| 5 | 1. Unit — _note_title: frontmatter title, H1 heading fallback, empty note |
| 6 | 2. Unit — _summarise_note_delta: tags added/removed, section add/remove/move, |
| 7 | scalar frontmatter change, body line changes, empty delta |
| 8 | 3. Unit — JSON schema: _KnowtationCompareJson has required top-level keys; |
| 9 | stat keys all present; each op has required fields |
| 10 | 4. Integration — _run_knowtation_compare: two synthetic manifests with one |
| 11 | added, one removed, one modified note → correct op list and stats |
| 12 | 5. Integration — JSON round-trip: output is valid JSON, deserialises correctly, |
| 13 | schema validated |
| 14 | 6. Integration — stat_only mode: prints note counts but not op details |
| 15 | 7. Security — path traversal in note paths; malformed YAML frontmatter; NUL |
| 16 | bytes in note content; object_store returning None |
| 17 | """ |
| 18 | |
| 19 | from __future__ import annotations |
| 20 | |
| 21 | import hashlib |
| 22 | import json |
| 23 | import pathlib |
| 24 | from typing import Any |
| 25 | from unittest.mock import MagicMock, patch |
| 26 | |
| 27 | import pytest |
| 28 | |
| 29 | # --------------------------------------------------------------------------- |
| 30 | # Helpers |
| 31 | # --------------------------------------------------------------------------- |
| 32 | |
| 33 | |
| 34 | def _make_note( |
| 35 | title: str = "Test Note", |
| 36 | tags: list[str] | None = None, |
| 37 | body: str = "Some body text.", |
| 38 | ) -> bytes: |
| 39 | fm_parts = [f"title: {title}"] |
| 40 | if tags: |
| 41 | tag_lines = "\n".join(f" - {t}" for t in tags) |
| 42 | fm_parts.append(f"tags:\n{tag_lines}") |
| 43 | fm = "\n".join(fm_parts) |
| 44 | return f"---\n{fm}\n---\n{body}".encode() |
| 45 | |
| 46 | |
| 47 | def _sha256(content: bytes) -> str: |
| 48 | return hashlib.sha256(content).hexdigest() |
| 49 | |
| 50 | |
| 51 | def _store_object(root: pathlib.Path, content: bytes) -> str: |
| 52 | """Write *content* to the Muse object store and return its SHA-256 hash.""" |
| 53 | from muse.core.object_store import write_object |
| 54 | |
| 55 | obj_id = _sha256(content) |
| 56 | write_object(root, obj_id, content) |
| 57 | return obj_id |
| 58 | |
| 59 | |
| 60 | # ============================================================================ |
| 61 | # TestNoteTitleHelper |
| 62 | # ============================================================================ |
| 63 | |
| 64 | |
| 65 | class TestNoteTitleHelper: |
| 66 | def test_frontmatter_title(self) -> None: |
| 67 | from muse.cli.commands.compare import _note_title |
| 68 | |
| 69 | note = _make_note(title="My Note Title") |
| 70 | assert _note_title(note) == "My Note Title" |
| 71 | |
| 72 | def test_h1_fallback_no_frontmatter(self) -> None: |
| 73 | from muse.cli.commands.compare import _note_title |
| 74 | |
| 75 | note = b"# Heading Title\n\nBody text." |
| 76 | assert _note_title(note) == "Heading Title" |
| 77 | |
| 78 | def test_empty_note_returns_empty_string(self) -> None: |
| 79 | from muse.cli.commands.compare import _note_title |
| 80 | |
| 81 | assert _note_title(b"") == "" |
| 82 | |
| 83 | def test_binary_content_does_not_crash(self) -> None: |
| 84 | from muse.cli.commands.compare import _note_title |
| 85 | |
| 86 | result = _note_title(bytes(range(256))) |
| 87 | assert isinstance(result, str) |
| 88 | |
| 89 | def test_frontmatter_title_preferred_over_h1(self) -> None: |
| 90 | from muse.cli.commands.compare import _note_title |
| 91 | |
| 92 | note = b"---\ntitle: FM Title\n---\n# H1 Title\n" |
| 93 | assert _note_title(note) == "FM Title" |
| 94 | |
| 95 | |
| 96 | # ============================================================================ |
| 97 | # TestSummariseNoteDelta |
| 98 | # ============================================================================ |
| 99 | |
| 100 | |
| 101 | class TestSummariseNoteDelta: |
| 102 | def test_empty_delta_returns_empty_diffs(self) -> None: |
| 103 | from muse.cli.commands.compare import _summarise_note_delta |
| 104 | |
| 105 | delta = {"ops": [], "summary": "no changes", "domain": "knowtation"} |
| 106 | fm, sec, body = _summarise_note_delta(delta) |
| 107 | assert fm == {} |
| 108 | assert sec == {} |
| 109 | assert body == 0 |
| 110 | |
| 111 | def test_tags_added(self) -> None: |
| 112 | from muse.cli.commands.compare import _summarise_note_delta |
| 113 | |
| 114 | delta = { |
| 115 | "ops": [ |
| 116 | {"op": "insert", "address": "tags::machine-learning"}, |
| 117 | {"op": "insert", "address": "tags::python"}, |
| 118 | ], |
| 119 | "domain": "knowtation", |
| 120 | } |
| 121 | fm, _, _ = _summarise_note_delta(delta) |
| 122 | assert "machine-learning" in fm.get("tags_added", []) |
| 123 | assert "python" in fm.get("tags_added", []) |
| 124 | |
| 125 | def test_tags_removed(self) -> None: |
| 126 | from muse.cli.commands.compare import _summarise_note_delta |
| 127 | |
| 128 | delta = { |
| 129 | "ops": [{"op": "delete", "address": "tags::old-tag"}], |
| 130 | "domain": "knowtation", |
| 131 | } |
| 132 | fm, _, _ = _summarise_note_delta(delta) |
| 133 | assert "old-tag" in fm.get("tags_removed", []) |
| 134 | |
| 135 | def test_section_added(self) -> None: |
| 136 | from muse.cli.commands.compare import _summarise_note_delta |
| 137 | |
| 138 | delta = { |
| 139 | "ops": [{"op": "insert", "address": "section:2:Background#0", "content": "..."}], |
| 140 | "domain": "knowtation", |
| 141 | } |
| 142 | _, sec, _ = _summarise_note_delta(delta) |
| 143 | assert "section:2:Background#0" in sec.get("added", []) |
| 144 | |
| 145 | def test_section_removed(self) -> None: |
| 146 | from muse.cli.commands.compare import _summarise_note_delta |
| 147 | |
| 148 | delta = { |
| 149 | "ops": [{"op": "delete", "address": "section:2:Deprecated#0"}], |
| 150 | "domain": "knowtation", |
| 151 | } |
| 152 | _, sec, _ = _summarise_note_delta(delta) |
| 153 | assert "section:2:Deprecated#0" in sec.get("removed", []) |
| 154 | |
| 155 | def test_section_moved(self) -> None: |
| 156 | from muse.cli.commands.compare import _summarise_note_delta |
| 157 | |
| 158 | delta = { |
| 159 | "ops": [{"op": "move", "address": "section:2:Background#0", "content_id": "abc"}], |
| 160 | "domain": "knowtation", |
| 161 | } |
| 162 | _, sec, _ = _summarise_note_delta(delta) |
| 163 | assert "section:2:Background#0" in sec.get("moved", []) |
| 164 | |
| 165 | def test_body_line_changes_counted(self) -> None: |
| 166 | from muse.cli.commands.compare import _summarise_note_delta |
| 167 | |
| 168 | delta = { |
| 169 | "ops": [ |
| 170 | {"op": "insert", "address": "section:2:Background#0::line:3"}, |
| 171 | {"op": "delete", "address": "section:2:Background#0::line:5"}, |
| 172 | {"op": "replace", "address": "section:2:Background#0::line:7"}, |
| 173 | ], |
| 174 | "domain": "knowtation", |
| 175 | } |
| 176 | _, _, body = _summarise_note_delta(delta) |
| 177 | assert body == 3 |
| 178 | |
| 179 | def test_scalar_frontmatter_change(self) -> None: |
| 180 | from muse.cli.commands.compare import _summarise_note_delta |
| 181 | |
| 182 | delta = { |
| 183 | "ops": [{"op": "replace", "address": "fm::project", "new_content": "new-project"}], |
| 184 | "domain": "knowtation", |
| 185 | } |
| 186 | fm, _, _ = _summarise_note_delta(delta) |
| 187 | assert "project" in fm.get("scalar_changes", {}) |
| 188 | |
| 189 | def test_non_dict_delta_handled_gracefully(self) -> None: |
| 190 | from muse.cli.commands.compare import _summarise_note_delta |
| 191 | |
| 192 | fm, sec, body = _summarise_note_delta(None) # type: ignore[arg-type] |
| 193 | assert fm == {} |
| 194 | assert sec == {} |
| 195 | assert body == 0 |
| 196 | |
| 197 | |
| 198 | # ============================================================================ |
| 199 | # TestKnowtationJsonSchema |
| 200 | # ============================================================================ |
| 201 | |
| 202 | |
| 203 | class TestKnowtationJsonSchema: |
| 204 | def _make_minimal_output(self) -> dict[str, Any]: |
| 205 | """Build a minimal _KnowtationCompareJson dict.""" |
| 206 | from muse.cli.commands.compare import _KnowtationCompareJson, _CommitRef, _KnowtationStat |
| 207 | |
| 208 | return { |
| 209 | "from": {"commit_id": "a" * 64, "message": "First commit"}, |
| 210 | "to": {"commit_id": "b" * 64, "message": "Second commit"}, |
| 211 | "domain": "knowtation", |
| 212 | "stat": { |
| 213 | "notes_added": 1, |
| 214 | "notes_removed": 0, |
| 215 | "notes_modified": 2, |
| 216 | "frontmatter_changes": 3, |
| 217 | "section_changes": 4, |
| 218 | "tag_changes": 1, |
| 219 | }, |
| 220 | "ops": [], |
| 221 | } |
| 222 | |
| 223 | def test_required_top_level_keys(self) -> None: |
| 224 | out = self._make_minimal_output() |
| 225 | for key in ("from", "to", "domain", "stat", "ops"): |
| 226 | assert key in out |
| 227 | |
| 228 | def test_domain_is_knowtation(self) -> None: |
| 229 | out = self._make_minimal_output() |
| 230 | assert out["domain"] == "knowtation" |
| 231 | |
| 232 | def test_stat_has_all_keys(self) -> None: |
| 233 | out = self._make_minimal_output() |
| 234 | stat = out["stat"] |
| 235 | for key in ( |
| 236 | "notes_added", "notes_removed", "notes_modified", |
| 237 | "frontmatter_changes", "section_changes", "tag_changes", |
| 238 | ): |
| 239 | assert key in stat, f"Missing stat key: {key}" |
| 240 | |
| 241 | def test_serialisable_to_json(self) -> None: |
| 242 | out = self._make_minimal_output() |
| 243 | dumped = json.dumps(out) |
| 244 | loaded = json.loads(dumped) |
| 245 | assert loaded["domain"] == "knowtation" |
| 246 | |
| 247 | |
| 248 | # ============================================================================ |
| 249 | # TestRunKnowtationCompare (integration) |
| 250 | # ============================================================================ |
| 251 | |
| 252 | |
| 253 | class TestRunKnowtationCompare: |
| 254 | """Integration tests for _run_knowtation_compare using a tmp root.""" |
| 255 | |
| 256 | def _setup_vault( |
| 257 | self, tmp_path: pathlib.Path |
| 258 | ) -> tuple[dict[str, str], dict[str, str], dict, dict]: |
| 259 | """Create a minimal vault layout in tmp_path and return two manifests.""" |
| 260 | # Three notes: note_a.md stays, note_b.md is modified, note_c.md is added |
| 261 | note_a_v1 = _make_note("Note A", tags=["old"], body="Version 1") |
| 262 | note_b_v1 = _make_note("Note B", tags=["original"], body="Old body.") |
| 263 | note_b_v2 = _make_note("Note B", tags=["original", "new-tag"], body="New body.") |
| 264 | note_c_v2 = _make_note("Note C", body="Newly added.") |
| 265 | |
| 266 | hash_a = _store_object(tmp_path, note_a_v1) |
| 267 | hash_b1 = _store_object(tmp_path, note_b_v1) |
| 268 | hash_b2 = _store_object(tmp_path, note_b_v2) |
| 269 | hash_c = _store_object(tmp_path, note_c_v2) |
| 270 | |
| 271 | manifest_1: dict[str, str] = { |
| 272 | "note_a.md": hash_a, |
| 273 | "note_b.md": hash_b1, |
| 274 | } |
| 275 | manifest_2: dict[str, str] = { |
| 276 | "note_a.md": hash_a, |
| 277 | "note_b.md": hash_b2, |
| 278 | "note_c.md": hash_c, |
| 279 | } |
| 280 | |
| 281 | commit_1 = MagicMock() |
| 282 | commit_1.commit_id = "a" * 64 |
| 283 | commit_1.message = "First commit" |
| 284 | |
| 285 | commit_2 = MagicMock() |
| 286 | commit_2.commit_id = "b" * 64 |
| 287 | commit_2.message = "Second commit" |
| 288 | |
| 289 | return manifest_1, manifest_2, commit_1, commit_2 |
| 290 | |
| 291 | def test_run_json_output_has_correct_structure( |
| 292 | self, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture |
| 293 | ) -> None: |
| 294 | from muse.cli.commands.compare import _run_knowtation_compare |
| 295 | |
| 296 | manifest_1, manifest_2, commit_1, commit_2 = self._setup_vault(tmp_path) |
| 297 | |
| 298 | _run_knowtation_compare( |
| 299 | root=tmp_path, |
| 300 | commit_a=commit_1, |
| 301 | commit_b=commit_2, |
| 302 | manifest_a=manifest_1, |
| 303 | manifest_b=manifest_2, |
| 304 | as_json=True, |
| 305 | stat_only=False, |
| 306 | ) |
| 307 | |
| 308 | captured = capsys.readouterr() |
| 309 | out = json.loads(captured.out) |
| 310 | assert out["domain"] == "knowtation" |
| 311 | assert "stat" in out |
| 312 | assert "ops" in out |
| 313 | assert out["from"]["commit_id"] == "a" * 64 |
| 314 | |
| 315 | def test_added_note_appears_as_insert( |
| 316 | self, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture |
| 317 | ) -> None: |
| 318 | from muse.cli.commands.compare import _run_knowtation_compare |
| 319 | |
| 320 | manifest_1, manifest_2, commit_1, commit_2 = self._setup_vault(tmp_path) |
| 321 | |
| 322 | _run_knowtation_compare( |
| 323 | root=tmp_path, |
| 324 | commit_a=commit_1, |
| 325 | commit_b=commit_2, |
| 326 | manifest_a=manifest_1, |
| 327 | manifest_b=manifest_2, |
| 328 | as_json=True, |
| 329 | stat_only=False, |
| 330 | ) |
| 331 | |
| 332 | out = json.loads(capsys.readouterr().out) |
| 333 | insert_ops = [o for o in out["ops"] if o["op"] == "insert"] |
| 334 | assert any(o["path"] == "note_c.md" for o in insert_ops) |
| 335 | |
| 336 | def test_modified_note_appears_as_modify( |
| 337 | self, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture |
| 338 | ) -> None: |
| 339 | from muse.cli.commands.compare import _run_knowtation_compare |
| 340 | |
| 341 | manifest_1, manifest_2, commit_1, commit_2 = self._setup_vault(tmp_path) |
| 342 | |
| 343 | _run_knowtation_compare( |
| 344 | root=tmp_path, |
| 345 | commit_a=commit_1, |
| 346 | commit_b=commit_2, |
| 347 | manifest_a=manifest_1, |
| 348 | manifest_b=manifest_2, |
| 349 | as_json=True, |
| 350 | stat_only=False, |
| 351 | ) |
| 352 | |
| 353 | out = json.loads(capsys.readouterr().out) |
| 354 | modify_ops = [o for o in out["ops"] if o["op"] == "modify"] |
| 355 | assert any(o["path"] == "note_b.md" for o in modify_ops) |
| 356 | |
| 357 | def test_unchanged_note_not_in_ops( |
| 358 | self, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture |
| 359 | ) -> None: |
| 360 | from muse.cli.commands.compare import _run_knowtation_compare |
| 361 | |
| 362 | manifest_1, manifest_2, commit_1, commit_2 = self._setup_vault(tmp_path) |
| 363 | |
| 364 | _run_knowtation_compare( |
| 365 | root=tmp_path, |
| 366 | commit_a=commit_1, |
| 367 | commit_b=commit_2, |
| 368 | manifest_a=manifest_1, |
| 369 | manifest_b=manifest_2, |
| 370 | as_json=True, |
| 371 | stat_only=False, |
| 372 | ) |
| 373 | |
| 374 | out = json.loads(capsys.readouterr().out) |
| 375 | assert all(o["path"] != "note_a.md" for o in out["ops"]) |
| 376 | |
| 377 | def test_stat_counts_correct( |
| 378 | self, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture |
| 379 | ) -> None: |
| 380 | from muse.cli.commands.compare import _run_knowtation_compare |
| 381 | |
| 382 | manifest_1, manifest_2, commit_1, commit_2 = self._setup_vault(tmp_path) |
| 383 | |
| 384 | _run_knowtation_compare( |
| 385 | root=tmp_path, |
| 386 | commit_a=commit_1, |
| 387 | commit_b=commit_2, |
| 388 | manifest_a=manifest_1, |
| 389 | manifest_b=manifest_2, |
| 390 | as_json=True, |
| 391 | stat_only=False, |
| 392 | ) |
| 393 | |
| 394 | out = json.loads(capsys.readouterr().out) |
| 395 | assert out["stat"]["notes_added"] == 1 # note_c added |
| 396 | assert out["stat"]["notes_modified"] == 1 # note_b modified |
| 397 | |
| 398 | def test_stat_only_mode_emits_text( |
| 399 | self, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture |
| 400 | ) -> None: |
| 401 | from muse.cli.commands.compare import _run_knowtation_compare |
| 402 | |
| 403 | manifest_1, manifest_2, commit_1, commit_2 = self._setup_vault(tmp_path) |
| 404 | |
| 405 | _run_knowtation_compare( |
| 406 | root=tmp_path, |
| 407 | commit_a=commit_1, |
| 408 | commit_b=commit_2, |
| 409 | manifest_a=manifest_1, |
| 410 | manifest_b=manifest_2, |
| 411 | as_json=False, |
| 412 | stat_only=True, |
| 413 | ) |
| 414 | |
| 415 | out = capsys.readouterr().out |
| 416 | assert "Notes added" in out |
| 417 | assert "Notes modified" in out |
| 418 | |
| 419 | def test_empty_manifests_no_crash( |
| 420 | self, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture |
| 421 | ) -> None: |
| 422 | from muse.cli.commands.compare import _run_knowtation_compare |
| 423 | |
| 424 | commit = MagicMock() |
| 425 | commit.commit_id = "a" * 64 |
| 426 | commit.message = "empty" |
| 427 | |
| 428 | _run_knowtation_compare( |
| 429 | root=tmp_path, |
| 430 | commit_a=commit, |
| 431 | commit_b=commit, |
| 432 | manifest_a={}, |
| 433 | manifest_b={}, |
| 434 | as_json=True, |
| 435 | stat_only=False, |
| 436 | ) |
| 437 | |
| 438 | out = json.loads(capsys.readouterr().out) |
| 439 | assert out["ops"] == [] |
| 440 | assert out["stat"]["notes_added"] == 0 |
| 441 | |
| 442 | def test_removed_note_appears_as_delete( |
| 443 | self, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture |
| 444 | ) -> None: |
| 445 | from muse.cli.commands.compare import _run_knowtation_compare |
| 446 | |
| 447 | note = _make_note("Gone Note") |
| 448 | h = _store_object(tmp_path, note) |
| 449 | |
| 450 | commit_a = MagicMock() |
| 451 | commit_a.commit_id = "a" * 64 |
| 452 | commit_a.message = "before" |
| 453 | commit_b = MagicMock() |
| 454 | commit_b.commit_id = "b" * 64 |
| 455 | commit_b.message = "after" |
| 456 | |
| 457 | _run_knowtation_compare( |
| 458 | root=tmp_path, |
| 459 | commit_a=commit_a, |
| 460 | commit_b=commit_b, |
| 461 | manifest_a={"gone.md": h}, |
| 462 | manifest_b={}, |
| 463 | as_json=True, |
| 464 | stat_only=False, |
| 465 | ) |
| 466 | |
| 467 | out = json.loads(capsys.readouterr().out) |
| 468 | delete_ops = [o for o in out["ops"] if o["op"] == "delete"] |
| 469 | assert any(o["path"] == "gone.md" for o in delete_ops) |
| 470 | assert out["stat"]["notes_removed"] == 1 |
| 471 | |
| 472 | |
| 473 | # ============================================================================ |
| 474 | # TestSecurity |
| 475 | # ============================================================================ |
| 476 | |
| 477 | |
| 478 | class TestSecurity: |
| 479 | def test_path_traversal_in_manifest_key( |
| 480 | self, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture |
| 481 | ) -> None: |
| 482 | """Paths with .. in them should not cause os.path traversal issues.""" |
| 483 | from muse.cli.commands.compare import _run_knowtation_compare |
| 484 | |
| 485 | commit = MagicMock() |
| 486 | commit.commit_id = "a" * 64 |
| 487 | commit.message = "test" |
| 488 | |
| 489 | # Object does not exist for this hash — should not crash |
| 490 | _run_knowtation_compare( |
| 491 | root=tmp_path, |
| 492 | commit_a=commit, |
| 493 | commit_b=commit, |
| 494 | manifest_a={"../../etc/passwd.md": "a" * 64}, |
| 495 | manifest_b={"../../etc/passwd.md": "b" * 64}, |
| 496 | as_json=True, |
| 497 | stat_only=False, |
| 498 | ) |
| 499 | |
| 500 | out_text = capsys.readouterr().out |
| 501 | # Should either produce valid JSON or be empty — must not crash |
| 502 | if out_text.strip(): |
| 503 | json.loads(out_text) # must be valid JSON |
| 504 | |
| 505 | def test_malformed_yaml_in_note_does_not_crash( |
| 506 | self, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture |
| 507 | ) -> None: |
| 508 | from muse.cli.commands.compare import _run_knowtation_compare |
| 509 | |
| 510 | malformed = b"---\ntitle: [unclosed\n---\nBody." |
| 511 | h1 = _store_object(tmp_path, malformed) |
| 512 | fixed = b"---\ntitle: fixed\n---\nBody." |
| 513 | h2 = _store_object(tmp_path, fixed) |
| 514 | |
| 515 | commit_a = MagicMock() |
| 516 | commit_a.commit_id = "a" * 64 |
| 517 | commit_a.message = "before" |
| 518 | commit_b = MagicMock() |
| 519 | commit_b.commit_id = "b" * 64 |
| 520 | commit_b.message = "after" |
| 521 | |
| 522 | try: |
| 523 | _run_knowtation_compare( |
| 524 | root=tmp_path, |
| 525 | commit_a=commit_a, |
| 526 | commit_b=commit_b, |
| 527 | manifest_a={"note.md": h1}, |
| 528 | manifest_b={"note.md": h2}, |
| 529 | as_json=True, |
| 530 | stat_only=False, |
| 531 | ) |
| 532 | except Exception as exc: |
| 533 | pytest.fail(f"Should not crash on malformed YAML: {exc}") |
| 534 | |
| 535 | def test_nul_bytes_in_note_content_handled( |
| 536 | self, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture |
| 537 | ) -> None: |
| 538 | from muse.cli.commands.compare import _run_knowtation_compare |
| 539 | |
| 540 | note_v1 = b"---\ntitle: NUL test\n---\n\x00\x00\x00" |
| 541 | note_v2 = b"---\ntitle: NUL test\n---\n\x00\x01\x02" |
| 542 | h1 = _store_object(tmp_path, note_v1) |
| 543 | h2 = _store_object(tmp_path, note_v2) |
| 544 | |
| 545 | commit_a = MagicMock() |
| 546 | commit_a.commit_id = "a" * 64 |
| 547 | commit_a.message = "a" |
| 548 | commit_b = MagicMock() |
| 549 | commit_b.commit_id = "b" * 64 |
| 550 | commit_b.message = "b" |
| 551 | |
| 552 | try: |
| 553 | _run_knowtation_compare( |
| 554 | root=tmp_path, |
| 555 | commit_a=commit_a, |
| 556 | commit_b=commit_b, |
| 557 | manifest_a={"nul.md": h1}, |
| 558 | manifest_b={"nul.md": h2}, |
| 559 | as_json=True, |
| 560 | stat_only=False, |
| 561 | ) |
| 562 | except Exception as exc: |
| 563 | pytest.fail(f"NUL bytes caused crash: {exc}") |
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