import errno import os import stat import subprocess import sys from copy import deepcopy from datetime import datetime, timedelta, timezone from pathlib import Path from unittest.mock import MagicMock, patch import pytest import yaml from semantica.context.agent_memory import AgentMemory _ERROR_PRIVILEGE_NOT_HELD = 1314 class TrackingVectorStore: def __init__(self): self.items = {} self.events = [] self.fail_after_add = False def add(self, items): memory_ids = [] for item in items: self.items[item.memory_id] = deepcopy(item) memory_ids.append(item.memory_id) self.events.append(("add", memory_ids)) if self.fail_after_add: raise RuntimeError("vector add failed after mutation") return memory_ids def delete(self, memory_ids): self.events.append(("delete", list(memory_ids))) for memory_id in memory_ids: self.items.pop(memory_id, None) return True class TrackingConcreteVectorStore: def __init__(self): self.events = [] self.next_id = 0 def store_vectors(self, vectors, metadata): vector_id = f"vec_{self.next_id}" self.next_id += 1 self.events.append(("store", vector_id)) return [vector_id] def delete_vectors(self, vector_ids): self.events.append(("delete", list(vector_ids))) return True def markdown_document(frontmatter, body=""): yaml_text = yaml.safe_dump(frontmatter, sort_keys=False, allow_unicode=True) return f"---\n{yaml_text}---\n\n{body}" def _require_symlink_support(tmp_path): """Skip the test if this environment cannot create symbolic links. Symlink creation can be unavailable even on POSIX (e.g. restricted containers) and commonly requires elevated privilege or Developer Mode on Windows. Probe actual capability instead of assuming based on platform, so these tests still run wherever symlinks genuinely work. """ probe_target = tmp_path / ".symlink_probe_target" probe_link = tmp_path / ".symlink_probe_link" probe_target.write_text("", encoding="utf-8") try: probe_link.symlink_to(probe_target) except OSError as exc: pytest.skip(f"environment cannot create symbolic links: {exc}") probe_link.unlink() probe_target.unlink() def required_frontmatter(memory_id="mem_test", **overrides): frontmatter = { "id": memory_id, "created_at": "2026-07-22T09:00:00+00:00", "updated_at": "2026-07-22T10:00:00+00:00", "type": "note", } frontmatter.update(overrides) return frontmatter def test_markdown_round_trip_is_lossless_and_deterministic(): memory = AgentMemory() content = "\n# Editable memory\n\n\n\n" memory.store( content, metadata={ "type": "note", "updated_at": "2026-07-22T12:30:00+00:00", "source": "caf\u00e9 review", "tags": ["alpha", "beta"], "id": "metadata-id", "kind": "metadata-kind", }, entities=[{"id": "ent_1", "type": "topic", "text": "Memory"}], relationships=[ { "source_id": "ent_1", "target_id": "ent_2", "type": "related_to", "confidence": 0.9, } ], memory_id="mem_round_trip", timestamp=datetime.fromisoformat("2026-07-22T12:00:00+00:00"), ) exported = memory.export(format="markdown") restored = AgentMemory() assert restored.import_data(exported, format="markdown") == 1 assert restored.get("mem_round_trip") == memory.get("mem_round_trip") assert restored.export(format="markdown") == exported assert "caf\u00e9 review" in exported def test_markdown_directory_round_trip_uses_one_stable_file_per_memory(tmp_path): memory = AgentMemory() for index in range(2): timestamp = f"2026-07-22T0{index + 8}:00:00" memory.store( f"Memory {index}", metadata={ "type": "reference", "updated_at": timestamp, "source": f"source-{index}", }, memory_id=f"mem_{index}", timestamp=datetime.fromisoformat(timestamp), ) first_export = tmp_path / "first" assert memory.export(format="markdown", destination=first_export) == str( first_export ) first_files = sorted(first_export.glob("*.md")) assert len(first_files) == 2 restored = AgentMemory() assert restored.import_data(first_export, format="markdown") == 2 assert set(restored.memory_items) == {"mem_0", "mem_1"} second_export = tmp_path / "second" restored.export(format="markdown", destination=second_export) assert {path.name: path.read_text(encoding="utf-8") for path in first_files} == { path.name: path.read_text(encoding="utf-8") for path in sorted(second_export.glob("*.md")) } def test_multiple_memories_require_a_destination_but_filters_can_select_one(): memory = AgentMemory() for memory_type in ("note", "reference"): memory.store( memory_type, metadata={ "type": memory_type, "updated_at": "2026-07-22T10:00:00", }, memory_id=f"mem_{memory_type}", timestamp=datetime(2026, 7, 22, 9, 0, 0), ) with pytest.raises(ValueError, match="destination"): memory.export(format="markdown") exported = memory.export(format="markdown", type="note") assert "id: mem_note" in exported assert "id: mem_reference" not in exported def test_markdown_update_replaces_supported_fields_and_preserves_id(): memory = AgentMemory() memory.store( "Original content", metadata={ "type": "note", "updated_at": "2026-07-22T09:30:00", "remove_me": True, }, memory_id="mem_existing", timestamp=datetime(2026, 7, 22, 9, 0, 0), ) edited = markdown_document( required_frontmatter( "mem_existing", created_at="2026-07-22T09:15:00", updated_at="2026-07-22T11:00:00", type="decision", source="manual edit", ), "Updated content", ) assert memory.import_data(edited, format="markdown") == 1 updated = memory.get("mem_existing") assert memory.count() == 1 assert updated["memory_id"] == "mem_existing" assert updated["content"] == "Updated content" assert updated["timestamp"] == "2026-07-22T09:15:00" assert updated["metadata"] == { "type": "decision", "source": "manual edit", "updated_at": "2026-07-22T11:00:00", } assert memory.stats["items_by_type"] == {"decision": 1} assert [item.memory_id for item in memory.short_term_memory] == ["mem_existing"] def test_public_update_preserves_id_and_statistics(): memory = AgentMemory() memory.store( "Original", metadata={"type": "note"}, memory_id="mem_update", timestamp=datetime(2026, 7, 22, 9, 0, 0), ) assert memory.update("mem_update", content="Updated", metadata={"type": "decision"}) assert memory.get("mem_update")["content"] == "Updated" assert set(memory.memory_items) == {"mem_update"} assert list(memory.memory_index) == ["mem_update"] assert memory.stats["total_items"] == 1 assert memory.stats["items_by_type"] == {"decision": 1} def test_reusing_custom_id_replaces_item_without_statistics_drift(): vector_store = TrackingVectorStore() memory = AgentMemory(vector_store=vector_store) memory.store( "Original", metadata={"type": "note"}, memory_id="mem_reused", timestamp=datetime(2026, 7, 22, 9, 0, 0), ) vector_store.events.clear() memory.store( "Replacement", metadata={"type": "decision"}, memory_id="mem_reused", timestamp=datetime(2026, 7, 22, 10, 0, 0), ) assert memory.get("mem_reused")["content"] == "Replacement" assert list(memory.memory_index) == ["mem_reused"] assert [item.memory_id for item in memory.short_term_memory] == ["mem_reused"] assert memory.stats["total_items"] == 1 assert memory.stats["items_by_type"] == {"decision": 1} assert vector_store.items["mem_reused"].content == "Replacement" assert vector_store.events == [("add", ["mem_reused"])] assert memory.delete_memory("mem_reused") assert memory.stats["total_items"] == 0 assert memory.stats["items_by_type"] == {} assert vector_store.items == {} def test_reimporting_unchanged_markdown_does_not_rewrite_memory(): memory = AgentMemory() document = markdown_document(required_frontmatter(), "Unchanged") assert memory.import_data(document, format="markdown") == 1 before = memory.get("mem_test") with patch.object( memory, "_replace_memory_item", wraps=memory._replace_memory_item ) as replace: assert memory.import_data(document, format="markdown") == 1 replace.assert_not_called() assert memory.get("mem_test") == before assert memory.count() == 1 def test_naive_and_aware_equivalent_timestamps_match_idempotently(): memory = AgentMemory() utc_dt = datetime(2026, 7, 22, 9, 0, 0, tzinfo=timezone.utc) naive_dt = utc_dt.astimezone().replace(tzinfo=None) memory.store( "Unchanged", metadata={"type": "note", "updated_at": "2026-07-22T10:00:00+00:00"}, memory_id="mem_test", timestamp=naive_dt, ) document = markdown_document( required_frontmatter("mem_test", created_at="2026-07-22T09:00:00+00:00"), "Unchanged", ) with patch.object( memory, "_replace_memory_item", wraps=memory._replace_memory_item ) as replace: assert memory.import_data(document, format="markdown") == 1 replace.assert_not_called() def test_kind_is_accepted_as_the_type_alias(): frontmatter = required_frontmatter() frontmatter["kind"] = frontmatter.pop("type") memory = AgentMemory() assert ( memory.import_data( markdown_document(frontmatter, "Kind alias"), format="markdown" ) == 1 ) assert memory.get("mem_test")["metadata"]["type"] == "note" @pytest.mark.parametrize("missing_field", ["id", "created_at", "updated_at", "type"]) def test_required_frontmatter_fields_are_enforced(missing_field): frontmatter = required_frontmatter() frontmatter.pop(missing_field) with pytest.raises(ValueError, match="missing required"): AgentMemory().import_data( markdown_document(frontmatter, "Invalid"), format="markdown" ) @pytest.mark.parametrize( ("overrides", "message"), [ ({"id": 123}, "'id' must be a string"), ({"created_at": "yesterday"}, "ISO-8601"), ({"updated_at": "later"}, "ISO-8601"), ({"type": ["note"]}, "must be a string"), ({"kind": "decision"}, "must match"), ({"metadata": []}, "must be a mapping"), ({"entities": {}}, "must be a list"), ({"entities": ["entity"]}, "must be a mapping"), ({"relationships": [1]}, "must be a mapping"), ({"metadata": {"type": "nested"}}, "duplicates reserved"), ({"metadata": {"source": "nested"}, "source": "top"}, "defined both"), ], ) def test_invalid_frontmatter_values_return_actionable_errors(overrides, message): document = markdown_document(required_frontmatter(**overrides), "Invalid") with pytest.raises(ValueError, match=message): AgentMemory().import_data(document, format="markdown") @pytest.mark.parametrize( "document", [ "not frontmatter", "---\nid: broken", "---\nid: [unterminated\n---\n", ( "---\nid: first\nid: second\ncreated_at: 2026-07-22T09:00:00\n" "updated_at: 2026-07-22T10:00:00\ntype: note\n---\n" ), ], ) def test_malformed_frontmatter_is_rejected(document): with pytest.raises(ValueError, match="frontmatter"): AgentMemory().import_data(document, format="markdown") def test_directory_is_fully_validated_before_any_memory_is_changed(tmp_path): valid = markdown_document(required_frontmatter("mem_valid"), "Valid") invalid_fields = required_frontmatter("mem_invalid") invalid_fields.pop("updated_at") invalid = markdown_document(invalid_fields, "Invalid") (tmp_path / "a-valid.md").write_text(valid, encoding="utf-8") (tmp_path / "z-invalid.md").write_text(invalid, encoding="utf-8") memory = AgentMemory() with pytest.raises(ValueError, match="updated_at"): memory.import_data(tmp_path, format="markdown") assert memory.count() == 0 def test_duplicate_ids_in_a_directory_are_rejected_before_import(tmp_path): document = markdown_document(required_frontmatter("mem_duplicate"), "Body") (tmp_path / "first.md").write_text(document, encoding="utf-8") (tmp_path / "second.markdown").write_text(document, encoding="utf-8") memory = AgentMemory() with pytest.raises(ValueError, match="Duplicate Markdown memory ID"): memory.import_data(tmp_path, format="markdown") assert memory.count() == 0 def test_markdown_import_keeps_provenance_out_of_the_context_graph(): knowledge_graph = MagicMock() memory = AgentMemory(knowledge_graph=knowledge_graph) fields = required_frontmatter( entities=[{"id": "entity_1", "type": "topic"}], relationships=[ { "source_id": "entity_1", "target_id": "entity_2", "type": "related_to", } ], ) assert ( memory.import_data(markdown_document(fields, "Provenance"), format="markdown") == 1 ) assert memory.get("mem_test")["entities"][0]["id"] == "entity_1" knowledge_graph.add_nodes.assert_not_called() knowledge_graph.add_edges.assert_not_called() def test_operational_failure_restores_existing_in_memory_state(): memory = AgentMemory() memory.store( "Original", metadata={ "type": "note", "updated_at": "2026-07-22T09:30:00", }, memory_id="mem_existing", timestamp=datetime(2026, 7, 22, 9, 0, 0), ) before_memory = deepcopy(memory.get("mem_existing")) before_stats = deepcopy(memory.stats) edited_fields = required_frontmatter("mem_existing") original_store = memory.store def fail_after_store(*args, **kwargs): original_store(*args, **kwargs) raise RuntimeError("store unavailable") with patch.object(memory, "store", side_effect=fail_after_store): with pytest.raises(RuntimeError, match="store unavailable"): memory.import_data( markdown_document(edited_fields, "Edited"), format="markdown" ) assert memory.get("mem_existing") == before_memory assert memory.stats == before_stats assert list(memory.memory_index) == ["mem_existing"] assert [item.memory_id for item in memory.short_term_memory] == ["mem_existing"] def test_failed_markdown_update_does_not_mutate_vector_store_before_rollback(): vector_store = TrackingVectorStore() memory = AgentMemory(vector_store=vector_store) memory.store( "Original", metadata={"type": "note"}, memory_id="mem_existing", timestamp=datetime(2026, 7, 22, 9, 0, 0), ) vector_store.events.clear() original_store = memory.store def fail_after_local_store(*args, **kwargs): original_store(*args, **kwargs) raise RuntimeError("local store failed") with patch.object(memory, "store", side_effect=fail_after_local_store): with pytest.raises(RuntimeError, match="local store failed"): memory.import_data( markdown_document(required_frontmatter("mem_existing"), "Replacement"), format="markdown", ) assert memory.get("mem_existing")["content"] == "Original" assert vector_store.items["mem_existing"].content == "Original" assert vector_store.events == [] def test_vector_sync_runs_after_markdown_commit_and_never_triggers_rollback(): vector_store = TrackingVectorStore() memory = AgentMemory(vector_store=vector_store) memory.store( "Original", metadata={"type": "note"}, memory_id="mem_existing", timestamp=datetime(2026, 7, 22, 9, 0, 0), ) vector_store.events.clear() vector_store.fail_after_add = True assert ( memory.import_data( markdown_document(required_frontmatter("mem_existing"), "Replacement"), format="markdown", ) == 1 ) assert memory.get("mem_existing")["content"] == "Replacement" assert vector_store.items["mem_existing"].content == "Replacement" assert vector_store.events == [("add", ["mem_existing"])] def test_markdown_update_replaces_concrete_adapter_vector_id_after_commit(): vector_store = TrackingConcreteVectorStore() memory = AgentMemory(vector_store=vector_store) memory.store( "Original", metadata={"type": "note"}, memory_id="mem_existing", timestamp=datetime(2026, 7, 22, 9, 0, 0), ) assert memory._vector_ids == {"mem_existing": ["vec_0"]} vector_store.events.clear() assert ( memory.import_data( markdown_document(required_frontmatter("mem_existing"), "Replacement"), format="markdown", ) == 1 ) assert memory._vector_ids == {"mem_existing": ["vec_1"]} assert vector_store.events == [("store", "vec_1"), ("delete", ["vec_0"])] def test_vector_id_mapping_survives_save_and_load(tmp_path): vector_store = TrackingConcreteVectorStore() memory = AgentMemory(vector_store=vector_store) memory.store( "Persisted", metadata={"type": "note"}, memory_id="mem_persisted", ) memory.save(str(tmp_path)) restored = AgentMemory(vector_store=vector_store) restored.load(str(tmp_path)) vector_store.events.clear() assert restored._vector_ids == {"mem_persisted": ["vec_0"]} assert restored.delete_memory("mem_persisted") assert vector_store.events == [("delete", ["vec_0"])] def test_failed_multi_file_import_never_starts_vector_synchronization(tmp_path): vector_store = TrackingVectorStore() memory = AgentMemory(vector_store=vector_store) for name in ("a-first.md", "b-second.md"): memory_id = name[:-3] (tmp_path / name).write_text( markdown_document(required_frontmatter(memory_id), name), encoding="utf-8", ) original_store = memory.store calls = 0 def fail_on_second_store(*args, **kwargs): nonlocal calls calls += 1 stored_id = original_store(*args, **kwargs) if calls == 2: raise RuntimeError("second local store failed") return stored_id with patch.object(memory, "store", side_effect=fail_on_second_store): with pytest.raises(RuntimeError, match="second local store failed"): memory.import_data(tmp_path, format="markdown") assert memory.count() == 0 assert vector_store.events == [] assert vector_store.items == {} def test_import_does_not_report_retention_pruned_memory_as_successful(): memory = AgentMemory(retention_policy="1_days") fields = required_frontmatter( "mem_expired", created_at="2000-01-01T00:00:00", updated_at="2000-01-01T00:00:00", ) with pytest.raises(RuntimeError, match="did not confirm"): memory.import_data(markdown_document(fields, "Expired"), format="markdown") assert memory.count() == 0 assert memory.stats["total_items"] == 0 def test_timezone_aware_import_supports_retention_sorting_and_date_filters(): now_utc = datetime.now(timezone.utc) memory = AgentMemory(retention_policy="1_days") memory.store( "Naive memory", metadata={"type": "note"}, memory_id="mem_naive", timestamp=datetime.now(), ) fields = required_frontmatter( "mem_aware", created_at=now_utc.isoformat(), updated_at=now_utc.isoformat(), ) assert ( memory.import_data(markdown_document(fields, "Aware memory"), "markdown") == 1 ) assert memory.get("mem_aware")["timestamp"].endswith("+00:00") assert {item["memory_id"] for item in memory.get_recent()} == { "mem_naive", "mem_aware", } start = now_utc - timedelta(hours=1) end = now_utc + timedelta(hours=1) assert {item["memory_id"] for item in memory.get_by_date(start, end)} == { "mem_naive", "mem_aware", } assert { item["memory_id"] for item in memory.retrieve( "memory", start_date=start.isoformat(), end_date=end.isoformat() ) } == {"mem_naive", "mem_aware"} def test_exported_filenames_cannot_escape_or_collide_with_destination(tmp_path): memory = AgentMemory() for memory_id in ("../outside", "a/b", "a\\b", "A/B"): memory.store( memory_id, metadata={ "type": "note", "updated_at": "2026-07-22T10:00:00", }, memory_id=memory_id, timestamp=datetime(2026, 7, 22, 9, 0, 0), ) destination = tmp_path / "export" memory.export(format="markdown", destination=destination) exported_files = list(destination.glob("*.md")) assert len(exported_files) == 4 assert len({path.name.casefold() for path in exported_files}) == 4 assert all(path.parent == destination for path in exported_files) assert not (tmp_path / "outside.md").exists() def test_markdown_export_rejects_symlink_without_touching_target(tmp_path): memory = AgentMemory() memory.store( "Protected content", metadata={"type": "note", "updated_at": "2026-07-22T10:00:00"}, memory_id="mem_symlink", timestamp=datetime(2026, 7, 22, 9, 0, 0), ) destination = tmp_path / "export" destination.mkdir() outside = tmp_path / "outside.md" outside.write_text("do not overwrite", encoding="utf-8") output_path = destination / memory._memory_markdown_filename("mem_symlink") try: output_path.symlink_to(outside) except OSError as error: winerror = getattr(error, "winerror", None) if sys.platform == "win32" and winerror == _ERROR_PRIVILEGE_NOT_HELD: pytest.skip("Windows symlink creation requires an unavailable privilege") raise with pytest.raises(ValueError, match="symbolic link"): memory.export(format="markdown", destination=destination) assert output_path.is_symlink() assert outside.read_text(encoding="utf-8") == "do not overwrite" def test_empty_markdown_export_and_import_are_no_ops(): memory = AgentMemory() assert memory.export(format="markdown") == "" assert memory.import_data("", format="markdown") == 0 def test_markdown_string_path_read_errors_are_not_treated_as_content(tmp_path): source = tmp_path / "memory.md" source.write_text( markdown_document(required_frontmatter(), "Content"), encoding="utf-8" ) memory = AgentMemory() with patch.object( memory, "_read_markdown_path", side_effect=PermissionError("read denied"), ): with pytest.raises(PermissionError, match="read denied"): memory.import_data(str(source), format="markdown") def test_markdown_string_path_inspection_errors_are_actionable(): memory = AgentMemory() original_error = PermissionError( errno.EACCES, "inspection denied", "blocked-memory.md", ) with patch( "semantica.context.agent_memory.Path.exists", side_effect=original_error, ): with pytest.raises( OSError, match="Failed to inspect possible Markdown import path" ) as exc_info: memory.import_data("memory.md", format="markdown") assert exc_info.value.errno == errno.EACCES assert exc_info.value.filename == "blocked-memory.md" assert "inspection denied" in str(exc_info.value) assert exc_info.value.__cause__ is original_error @pytest.mark.parametrize("use_string_path", [False, True]) def test_markdown_import_rejects_symlinked_file(tmp_path, use_string_path): _require_symlink_support(tmp_path) outside = tmp_path / "outside.md" outside.write_text( markdown_document(required_frontmatter(), "Do not import"), encoding="utf-8", ) source = tmp_path / "memory.md" source.symlink_to(outside) payload = str(source) if use_string_path else source memory = AgentMemory() with pytest.raises(ValueError, match="symbolic link"): memory.import_data(payload, format="markdown") assert memory.count() == 0 @pytest.mark.parametrize("use_string_path", [False, True]) def test_markdown_import_rejects_broken_symlink(tmp_path, use_string_path): _require_symlink_support(tmp_path) source = tmp_path / "missing-memory.md" source.symlink_to(tmp_path / "missing-target.md") payload = str(source) if use_string_path else source memory = AgentMemory() with pytest.raises(ValueError, match="symbolic link"): memory.import_data(payload, format="markdown") assert memory.count() == 0 @pytest.mark.parametrize("use_string_path", [False, True]) def test_markdown_import_rejects_symlinked_directory(tmp_path, use_string_path): _require_symlink_support(tmp_path) outside = tmp_path / "outside" outside.mkdir() (outside / "memory.md").write_text( markdown_document(required_frontmatter(), "Do not import"), encoding="utf-8", ) source = tmp_path / "memory-export" source.symlink_to(outside, target_is_directory=True) payload = str(source) if use_string_path else source memory = AgentMemory() with pytest.raises(ValueError, match="symbolic link"): memory.import_data(payload, format="markdown") assert memory.count() == 0 def test_markdown_import_skips_symlinked_file_in_directory(tmp_path): _require_symlink_support(tmp_path) outside = tmp_path / "outside.md" outside.write_text( markdown_document(required_frontmatter(), "Do not import"), encoding="utf-8", ) source = tmp_path / "memory-export" source.mkdir() (source / "memory.md").symlink_to(outside) memory = AgentMemory() assert memory.import_data(source, format="markdown") == 0 assert memory.count() == 0 @pytest.mark.skipif(not hasattr(os, "O_NOFOLLOW"), reason="requires O_NOFOLLOW") def test_markdown_import_does_not_follow_symlink_raced_before_open(tmp_path): _require_symlink_support(tmp_path) outside = tmp_path / "outside.md" outside.write_text( markdown_document(required_frontmatter(), "Do not import"), encoding="utf-8", ) source = tmp_path / "memory.md" source.symlink_to(outside) with patch.object(Path, "is_symlink", return_value=False): with pytest.raises(ValueError, match="symbolic link"): AgentMemory()._read_markdown_file_content(source) def test_markdown_import_rejects_windows_junction(tmp_path, monkeypatch): source = tmp_path / "junction" source.mkdir() (source / "memory.md").write_text("not read", encoding="utf-8") monkeypatch.setattr( os.path, "isjunction", lambda candidate: Path(candidate) == source, raising=False, ) with pytest.raises(ValueError, match="junction"): AgentMemory().import_data(source, format="markdown") def test_markdown_import_rejects_windows_reparse_point_fallback(tmp_path, monkeypatch): source = tmp_path / "reparse-point" source.mkdir() real_lstat = os.lstat class ReparseStat: st_file_attributes = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400) monkeypatch.delattr(os.path, "isjunction", raising=False) monkeypatch.setattr(Path, "is_symlink", lambda self: False) monkeypatch.setattr( os, "lstat", lambda candidate: ( ReparseStat() if Path(candidate) == source else real_lstat(candidate) ), ) with pytest.raises(ValueError, match="junction"): AgentMemory().import_data(source, format="markdown") @pytest.mark.skipif(os.name != "nt", reason="requires Windows junctions") def test_markdown_import_rejects_real_windows_junction(tmp_path): outside = tmp_path / "outside" outside.mkdir() (outside / "memory.md").write_text("not read", encoding="utf-8") source = tmp_path / "junction" result = subprocess.run( ["cmd.exe", "/c", "mklink", "/J", str(source), str(outside)], capture_output=True, text=True, check=False, ) if result.returncode != 0: pytest.skip(f"could not create Windows junction: {result.stderr}") try: with pytest.raises(ValueError, match="junction"): AgentMemory().import_data(source, format="markdown") finally: os.rmdir(source) def test_legacy_dict_import_behavior_is_unchanged(): memory = AgentMemory() data = { "memories": [ { "memory_id": "source_id", "content": "Legacy import", "metadata": {"type": "note"}, "timestamp": "2000-01-01T00:00:00", } ] } assert memory.import_data(data, format="dict") == 1 assert not memory.exists("source_id") imported = next(iter(memory.memory_items.values())) assert imported.content == "Legacy import" assert imported.metadata == {"type": "note"} def test_markdown_export_destination_must_be_a_directory(tmp_path): destination = tmp_path / "memory.md" destination.write_text("occupied", encoding="utf-8") with pytest.raises(ValueError, match="not a directory"): AgentMemory().export(format="markdown", destination=destination) def test_markdown_import_file_open_security_rejects_symlink(tmp_path): memory = AgentMemory() target = tmp_path / "secret.txt" target.write_text("secret content", encoding="utf-8") symlink_file = tmp_path / "memory.md" try: symlink_file.symlink_to(target) except OSError as error: winerror = getattr(error, "winerror", None) if sys.platform == "win32" and winerror == _ERROR_PRIVILEGE_NOT_HELD: pytest.skip("Windows symlink creation requires an unavailable privilege") raise with pytest.raises(ValueError, match="Symlink Markdown import paths are rejected"): memory._read_markdown_file_content(symlink_file) def test_markdown_import_public_api_rejects_symlink(tmp_path): """ import_data(..., format="markdown") must propagate the symlink rejection through the full call chain: import_data → _import_markdown_payload → _read_markdown_path → _read_markdown_file_content. This complements test_markdown_import_file_open_security_rejects_symlink, which only tests the private helper. A future refactor that bypasses _read_markdown_file_content would silently stop being protected; this test catches that. """ target = tmp_path / "secret.txt" target.write_text("secret content", encoding="utf-8") symlink_file = tmp_path / "memory.md" try: symlink_file.symlink_to(target) except OSError as error: winerror = getattr(error, "winerror", None) if sys.platform == "win32" and winerror == _ERROR_PRIVILEGE_NOT_HELD: pytest.skip("Windows symlink creation requires an unavailable privilege") raise memory = AgentMemory() with pytest.raises(ValueError, match="Symlink Markdown import paths are rejected"): memory.import_data(symlink_file, format="markdown") def test_markdown_import_directory_silently_skips_symlinked_entries(tmp_path): """ When importing a directory, symlink entries must be silently excluded. Only real regular files must be read. This tests the filter in _read_markdown_path: not file_path.is_symlink() which was added by PR #932. """ # Write a real Markdown file in the directory real_md = tmp_path / "real.md" real_md.write_text( markdown_document(required_frontmatter(memory_id="dir-real"), "Real content"), encoding="utf-8", ) # Write the symlink target outside the directory target = tmp_path.parent / "outside.txt" target.write_text("must not be read", encoding="utf-8") link_md = tmp_path / "evil.md" try: link_md.symlink_to(target) except OSError as error: winerror = getattr(error, "winerror", None) if sys.platform == "win32" and winerror == _ERROR_PRIVILEGE_NOT_HELD: pytest.skip("Windows symlink creation requires an unavailable privilege") raise memory = AgentMemory() # Must succeed, returning only the real file results = memory._read_markdown_path(tmp_path) assert len(results) == 1, ( f"Expected 1 result (real.md only), got {len(results)}: " f"{[r[0] for r in results]}" ) assert "Real content" in results[0][1] def test_markdown_import_rejects_non_regular_file(tmp_path): """ _read_markdown_file_content must raise ValueError when the opened file descriptor does not refer to a regular file (S_ISREG fails). This tests the fstat()/S_ISREG guard, which is the defense-in-depth layer that catches special files (FIFOs, character devices) even when the is_symlink() pre-check passes. The test works on both POSIX and Windows because it mocks os.fstat rather than relying on platform-specific filesystem objects. """ import stat as stat_module real_file = tmp_path / "not_really_regular.md" real_file.write_text("some data", encoding="utf-8") # Build a mock stat result whose st_mode describes a FIFO (S_IFIFO). fake_stat = MagicMock() fake_stat.st_mode = stat_module.S_IFIFO | 0o600 # FIFO with rw permissions memory = AgentMemory() with patch("semantica.context.agent_memory.os.fstat", return_value=fake_stat): with pytest.raises(ValueError, match="not a regular file"): memory._read_markdown_file_content(real_file)