Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/nooa-memory/src/nooa_memory/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,5 +198,5 @@ def _validate_owner(cls, value: str | None) -> str | None:
observability: ObservabilityConfig = ObservabilityConfig()

def merge_with(self, **overrides: object) -> MemoryConfig:
"""Return a copy with top-level fields overridden."""
return self.model_copy(update=overrides)
"""Return a validated copy with top-level fields overridden."""
return type(self).model_validate({**self.model_dump(), **overrides})
36 changes: 36 additions & 0 deletions packages/nooa-memory/tests/memory/test_memory_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Configuration merging obeys the same validation rules as construction."""

import pytest
from nooa_memory.config import EmbeddingConfig, MemoryConfig
from pydantic import ValidationError


def test_merge_coerces_boolean_without_mutating_original():
"""Overrides use Pydantic coercion without changing the original configuration."""
original = MemoryConfig(enabled=True, path=":memory:")
merged = original.merge_with(enabled="false")
assert merged.enabled is False
assert merged.path == original.path
assert original.enabled is True


def test_merge_validates_nested_config_as_top_level_replacement():
"""Nested dictionaries become validated models using top-level replacement semantics."""
original = MemoryConfig(embedding=EmbeddingConfig(dim=64, batch_size=3))
merged = original.merge_with(embedding={"backend": "hashing", "dim": 16})
assert isinstance(merged.embedding, EmbeddingConfig)
assert merged.embedding.dim == 16
assert merged.embedding.batch_size == EmbeddingConfig().batch_size
assert original.embedding.dim == 64
assert original.embedding.batch_size == 3


@pytest.mark.parametrize(
"overrides", [{"owner": "invalid_owner"}, {"embedding": {"backend": "unknown"}}]
)
def test_merge_rejects_invalid_configuration(overrides):
"""Invalid owner and embedding settings fail during the merge, not at later use."""
with pytest.raises(ValidationError):
MemoryConfig().merge_with(**overrides)