mirror of
https://github.com/strands-agents/harness-sdk.git
synced 2026-10-02 02:44:48 +08:00
fix(memory)!: rename LocalMemoryStore to TestMemoryStore (#3123)
This commit is contained in:
@@ -9,7 +9,7 @@ from typing import Any
|
||||
|
||||
__all__ = [
|
||||
"BedrockKnowledgeBaseStore",
|
||||
"LocalMemoryStore",
|
||||
"TestMemoryStore",
|
||||
]
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ def __getattr__(name: str) -> Any:
|
||||
from .bedrock_knowledge_base import BedrockKnowledgeBaseStore
|
||||
|
||||
return BedrockKnowledgeBaseStore
|
||||
if name == "LocalMemoryStore":
|
||||
from .local import LocalMemoryStore
|
||||
if name == "TestMemoryStore":
|
||||
from .test_memory_store import TestMemoryStore
|
||||
|
||||
return LocalMemoryStore
|
||||
return TestMemoryStore
|
||||
raise AttributeError(f"cannot import name '{name}' from '{__name__}' ({__file__})")
|
||||
|
||||
+7
-7
@@ -5,17 +5,17 @@ resources required. Persists to disk by default so an agent remembers across res
|
||||
|
||||
Example:
|
||||
```python
|
||||
from strands.vended_memory_stores.local import LocalMemoryStore
|
||||
from strands.vended_memory_stores.test_memory_store import TestMemoryStore
|
||||
|
||||
store = LocalMemoryStore(name="notes")
|
||||
store = TestMemoryStore(name="notes")
|
||||
```
|
||||
"""
|
||||
|
||||
from .store import LocalMemoryStore
|
||||
from .types import LocalMemoryAddResult, LocalMemoryStoreConfig
|
||||
from .store import TestMemoryStore
|
||||
from .types import TestMemoryAddResult, TestMemoryStoreConfig
|
||||
|
||||
__all__ = [
|
||||
"LocalMemoryAddResult",
|
||||
"LocalMemoryStore",
|
||||
"LocalMemoryStoreConfig",
|
||||
"TestMemoryAddResult",
|
||||
"TestMemoryStore",
|
||||
"TestMemoryStoreConfig",
|
||||
]
|
||||
+25
-22
@@ -17,7 +17,7 @@ from typing import Any
|
||||
from typing_extensions import Unpack
|
||||
|
||||
from ...memory.types import MemoryEntry, MemoryStore, Metadata, SearchOptions
|
||||
from .types import LocalMemoryAddResult, LocalMemoryStoreConfig
|
||||
from .types import TestMemoryAddResult, TestMemoryStoreConfig
|
||||
|
||||
DEFAULT_MAX_SEARCH_RESULTS = 10
|
||||
|
||||
@@ -68,11 +68,11 @@ def _token_overlap_score(query_tokens: set[str], content: str) -> int:
|
||||
return len(query_tokens & _tokenize(content))
|
||||
|
||||
|
||||
class LocalMemoryStore(MemoryStore):
|
||||
class TestMemoryStore(MemoryStore):
|
||||
"""A :class:`~strands.memory.types.MemoryStore` backed by an in-memory list and a local JSON file.
|
||||
|
||||
A zero-infrastructure store for prototyping and testing. It persists to disk by default so memories persist
|
||||
accross sessions. Set ``persist=False`` for an ephemeral, single-session store.
|
||||
across sessions. Set ``persist=False`` for an ephemeral, single-session store.
|
||||
|
||||
Recall is lexical: results are ranked by how many query tokens overlap an entry's content, with
|
||||
the most recent entry winning ties. This is keyword matching, not the semantic search a managed
|
||||
@@ -83,27 +83,30 @@ class LocalMemoryStore(MemoryStore):
|
||||
of entries), not production workloads — use a managed store like ``BedrockKnowledgeBaseStore`` for
|
||||
that. Writes within a process are serialized; concurrent writers across processes are not.
|
||||
|
||||
The on-disk format is shared with the TypeScript SDK's ``LocalMemoryStore``: records use the same
|
||||
The on-disk format is shared with the TypeScript SDK's ``TestMemoryStore``: records use the same
|
||||
camelCase keys (``id``, ``content``, ``metadata``, ``createdAt``) and the same timestamp shape, so
|
||||
a backing file written by either SDK can be read by the other.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from strands.vended_memory_stores.local import LocalMemoryStore
|
||||
from strands.vended_memory_stores.test_memory_store import TestMemoryStore
|
||||
|
||||
# Persists to ~/.strands/memory/notes.json by default.
|
||||
store = LocalMemoryStore(name="notes")
|
||||
store = TestMemoryStore(name="notes")
|
||||
|
||||
result = await store.add("User prefers dark mode")
|
||||
results = await store.search("what theme does the user like?")
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, **store_config: Unpack[LocalMemoryStoreConfig]) -> None:
|
||||
# Tell pytest not to collect this class as a test suite despite its ``Test`` prefix.
|
||||
__test__ = False
|
||||
|
||||
def __init__(self, **store_config: Unpack[TestMemoryStoreConfig]) -> None:
|
||||
"""Initialize the store.
|
||||
|
||||
Args:
|
||||
**store_config: See :class:`LocalMemoryStoreConfig`.
|
||||
**store_config: See :class:`TestMemoryStoreConfig`.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``name`` or ``path`` is empty/whitespace, or ``max_search_results`` is
|
||||
@@ -111,11 +114,11 @@ class LocalMemoryStore(MemoryStore):
|
||||
"""
|
||||
self.name = store_config["name"]
|
||||
if not self.name.strip():
|
||||
raise ValueError("LocalMemoryStore: name must not be empty.")
|
||||
raise ValueError("TestMemoryStore: name must not be empty.")
|
||||
self.description = store_config.get("description")
|
||||
max_search_results = store_config.get("max_search_results")
|
||||
if max_search_results is not None and max_search_results < 1:
|
||||
raise ValueError("LocalMemoryStore: max_search_results must be at least 1.")
|
||||
raise ValueError("TestMemoryStore: max_search_results must be at least 1.")
|
||||
self.max_search_results = max_search_results
|
||||
# A local store is writable by default: the point is a zero-setup store you can write to.
|
||||
self.writable = store_config.get("writable", True)
|
||||
@@ -124,7 +127,7 @@ class LocalMemoryStore(MemoryStore):
|
||||
self._persist = store_config.get("persist", True)
|
||||
path = store_config.get("path")
|
||||
if path is not None and not path.strip():
|
||||
raise ValueError("LocalMemoryStore: path must not be empty.")
|
||||
raise ValueError("TestMemoryStore: path must not be empty.")
|
||||
if not self._persist:
|
||||
self._path: Path | None = None
|
||||
elif path is not None:
|
||||
@@ -155,7 +158,7 @@ class LocalMemoryStore(MemoryStore):
|
||||
"""
|
||||
caller_max = options.get("max_search_results") if options is not None else None
|
||||
if caller_max is not None and caller_max < 1:
|
||||
raise ValueError("LocalMemoryStore: max_search_results must be at least 1.")
|
||||
raise ValueError("TestMemoryStore: max_search_results must be at least 1.")
|
||||
limit = caller_max or self.max_search_results or DEFAULT_MAX_SEARCH_RESULTS
|
||||
|
||||
query_tokens = _tokenize(query)
|
||||
@@ -178,7 +181,7 @@ class LocalMemoryStore(MemoryStore):
|
||||
entries.append(MemoryEntry(content=record["content"], metadata=metadata))
|
||||
return entries
|
||||
|
||||
async def add(self, content: str, metadata: Metadata | None = None) -> LocalMemoryAddResult:
|
||||
async def add(self, content: str, metadata: Metadata | None = None) -> TestMemoryAddResult:
|
||||
"""Add ``content`` (with optional ``metadata``) to the store.
|
||||
|
||||
Identical content is deduplicated: a repeat write returns the existing record's id without
|
||||
@@ -200,9 +203,9 @@ class LocalMemoryStore(MemoryStore):
|
||||
writable), with the target path in the message.
|
||||
"""
|
||||
if not self.writable:
|
||||
raise ValueError("LocalMemoryStore: store is not writable. Set writable=True in config to enable add().")
|
||||
raise ValueError("TestMemoryStore: store is not writable. Set writable=True in config to enable add().")
|
||||
if not content.strip():
|
||||
raise ValueError("LocalMemoryStore: content must not be empty.")
|
||||
raise ValueError("TestMemoryStore: content must not be empty.")
|
||||
|
||||
# The lock serializes the whole load-modify-flush cycle so concurrent adds don't each load
|
||||
# the same snapshot and clobber one another (last-write-wins). Within a single event loop the
|
||||
@@ -214,7 +217,7 @@ class LocalMemoryStore(MemoryStore):
|
||||
normalized_content = content.strip()
|
||||
for record in records:
|
||||
if record["content"].strip() == normalized_content:
|
||||
return LocalMemoryAddResult(id=record["id"])
|
||||
return TestMemoryAddResult(id=record["id"])
|
||||
|
||||
new_record: dict[str, Any] = {"id": _new_id(), "content": content, "createdAt": _now()}
|
||||
if metadata is not None:
|
||||
@@ -225,7 +228,7 @@ class LocalMemoryStore(MemoryStore):
|
||||
next_records = [*records, new_record]
|
||||
self._flush(next_records)
|
||||
self._records = next_records
|
||||
return LocalMemoryAddResult(id=new_record["id"])
|
||||
return TestMemoryAddResult(id=new_record["id"])
|
||||
|
||||
def _load(self) -> list[dict[str, Any]]:
|
||||
"""Load records from disk on first use; ephemeral stores (and a missing file) start empty."""
|
||||
@@ -243,12 +246,12 @@ class LocalMemoryStore(MemoryStore):
|
||||
self._records = []
|
||||
return self._records
|
||||
except json.JSONDecodeError as error:
|
||||
raise ValueError(f"LocalMemoryStore: invalid JSON in {self._path}: {error}") from error
|
||||
raise ValueError(f"TestMemoryStore: invalid JSON in {self._path}: {error}") from error
|
||||
except OSError as error:
|
||||
raise OSError(f"LocalMemoryStore: failed to read {self._path}: {error}") from error
|
||||
raise OSError(f"TestMemoryStore: failed to read {self._path}: {error}") from error
|
||||
|
||||
if not isinstance(parsed_file, list):
|
||||
raise ValueError(f"LocalMemoryStore: invalid backing file {self._path}: expected a JSON array of records")
|
||||
raise ValueError(f"TestMemoryStore: invalid backing file {self._path}: expected a JSON array of records")
|
||||
for record in parsed_file:
|
||||
if (
|
||||
not isinstance(record, dict)
|
||||
@@ -257,7 +260,7 @@ class LocalMemoryStore(MemoryStore):
|
||||
or not isinstance(record.get("createdAt"), str)
|
||||
):
|
||||
raise ValueError(
|
||||
f"LocalMemoryStore: invalid backing file {self._path}: "
|
||||
f"TestMemoryStore: invalid backing file {self._path}: "
|
||||
"each record must have string 'id', 'content', and 'createdAt' fields"
|
||||
)
|
||||
self._records = parsed_file
|
||||
@@ -282,4 +285,4 @@ class LocalMemoryStore(MemoryStore):
|
||||
json.dump(records, file, indent=2, ensure_ascii=False)
|
||||
tmp_path.replace(self._path)
|
||||
except OSError as error:
|
||||
raise OSError(f"LocalMemoryStore: failed to write {self._path}: {error}") from error
|
||||
raise OSError(f"TestMemoryStore: failed to write {self._path}: {error}") from error
|
||||
+13
-5
@@ -1,4 +1,4 @@
|
||||
"""Configuration and result types for the local memory store."""
|
||||
"""Configuration and result types for the JSON-file memory store."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -7,8 +7,8 @@ from dataclasses import dataclass
|
||||
from ...memory.types import MemoryStoreConfig
|
||||
|
||||
|
||||
class LocalMemoryStoreConfig(MemoryStoreConfig, total=False):
|
||||
"""Full configuration for a :class:`LocalMemoryStore`, passed as its constructor kwargs.
|
||||
class TestMemoryStoreConfig(MemoryStoreConfig, total=False):
|
||||
"""Full configuration for a :class:`TestMemoryStore`, passed as its constructor kwargs.
|
||||
|
||||
The store persists to disk by default so memories persist across sessions.
|
||||
Set ``persist`` to ``False`` for an ephemeral, single-session store.
|
||||
@@ -25,12 +25,20 @@ class LocalMemoryStoreConfig(MemoryStoreConfig, total=False):
|
||||
path: str
|
||||
|
||||
|
||||
# Tell pytest not to collect this class as a test suite despite its ``Test`` prefix. A TypedDict
|
||||
# rejects a ``__test__`` entry in its body, so it is assigned after the class instead.
|
||||
TestMemoryStoreConfig.__test__ = False # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@dataclass
|
||||
class LocalMemoryAddResult:
|
||||
"""Result returned by :meth:`LocalMemoryStore.add`.
|
||||
class TestMemoryAddResult:
|
||||
"""Result returned by :meth:`TestMemoryStore.add`.
|
||||
|
||||
Attributes:
|
||||
id: The generated id of the stored (or already-present, on dedup) record.
|
||||
"""
|
||||
|
||||
# Tell pytest not to collect this class as a test suite despite its ``Test`` prefix.
|
||||
__test__ = False
|
||||
|
||||
id: str
|
||||
+22
-10
@@ -1,4 +1,4 @@
|
||||
"""Tests for ``LocalMemoryStore``.
|
||||
"""Tests for ``TestMemoryStore``.
|
||||
|
||||
Test scaffolding:
|
||||
- ``tmp_path`` backs every persistent store, so tests never touch the real ``~/.strands/memory``.
|
||||
@@ -18,13 +18,17 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
import strands.vended_memory_stores.local.store as store_module
|
||||
import strands.vended_memory_stores.test_memory_store.store as store_module
|
||||
from strands.hooks.events import AfterInvocationEvent, MessageAddedEvent
|
||||
from strands.hooks.registry import HookOrder
|
||||
from strands.memory.extraction.triggers import InvocationTrigger
|
||||
from strands.memory.extraction.types import ExtractionConfig, ExtractionResult
|
||||
from strands.memory.memory_manager import MemoryManager
|
||||
from strands.vended_memory_stores.local import LocalMemoryStore
|
||||
from strands.vended_memory_stores.test_memory_store import (
|
||||
TestMemoryAddResult,
|
||||
TestMemoryStore,
|
||||
TestMemoryStoreConfig,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -37,10 +41,10 @@ def store_path(tmp_path: Path) -> str:
|
||||
def make_store(store_path: str):
|
||||
"""Factory building a persistent store at ``store_path`` with overridable config."""
|
||||
|
||||
def _make(**overrides: Any) -> LocalMemoryStore:
|
||||
def _make(**overrides: Any) -> TestMemoryStore:
|
||||
config: dict[str, Any] = {"name": "notes", "path": store_path}
|
||||
config.update(overrides)
|
||||
return LocalMemoryStore(**config)
|
||||
return TestMemoryStore(**config)
|
||||
|
||||
return _make
|
||||
|
||||
@@ -50,7 +54,7 @@ class TestPackageExport:
|
||||
# The store is documented as importable from the parent package via its lazy __getattr__.
|
||||
from strands.vended_memory_stores import __getattr__
|
||||
|
||||
assert __getattr__("LocalMemoryStore") is LocalMemoryStore
|
||||
assert __getattr__("TestMemoryStore") is TestMemoryStore
|
||||
|
||||
def test_unknown_attribute_raises(self):
|
||||
from strands.vended_memory_stores import __getattr__
|
||||
@@ -58,6 +62,14 @@ class TestPackageExport:
|
||||
with pytest.raises(AttributeError):
|
||||
__getattr__("NoSuchStore")
|
||||
|
||||
def test_test_prefixed_classes_are_not_collected_by_pytest(self):
|
||||
# The ``Test`` prefix would otherwise make pytest try to collect these classes as test suites
|
||||
# (emitting a PytestCollectionWarning). ``__test__ = False`` opts them out; assert it stays set
|
||||
# so a future edit that drops a guard fails here instead of silently re-enabling collection.
|
||||
assert TestMemoryStore.__test__ is False
|
||||
assert TestMemoryStoreConfig.__test__ is False
|
||||
assert TestMemoryAddResult.__test__ is False
|
||||
|
||||
|
||||
class TestConstructor:
|
||||
def test_is_writable_by_default(self, make_store):
|
||||
@@ -82,7 +94,7 @@ class TestConstructor:
|
||||
|
||||
def test_raises_when_explicit_path_is_empty(self):
|
||||
with pytest.raises(ValueError, match="path must not be empty"):
|
||||
LocalMemoryStore(name="notes", path=" ")
|
||||
TestMemoryStore(name="notes", path=" ")
|
||||
|
||||
def test_does_no_filesystem_io_on_construction(self, make_store, store_path):
|
||||
make_store()
|
||||
@@ -94,7 +106,7 @@ class TestConstructor:
|
||||
# redirected to tmp_path so the test never touches the real home dir, and the unsafe name
|
||||
# exercises sanitization.
|
||||
monkeypatch.setattr(store_module.Path, "home", classmethod(lambda cls: tmp_path))
|
||||
store = LocalMemoryStore(name="../weird/name")
|
||||
store = TestMemoryStore(name="../weird/name")
|
||||
await store.add("a fact worth keeping")
|
||||
expected = tmp_path / ".strands" / "memory" / "__weird_name.json"
|
||||
assert expected.is_file()
|
||||
@@ -274,7 +286,7 @@ class TestPersistence:
|
||||
# wrapped "failed to read/write" error naming the path rather than a bare OSError.
|
||||
blocker = tmp_path / "blocker"
|
||||
blocker.write_text("not a directory", encoding="utf-8")
|
||||
store = LocalMemoryStore(name="notes", path=str(blocker / "notes.json"))
|
||||
store = TestMemoryStore(name="notes", path=str(blocker / "notes.json"))
|
||||
with pytest.raises(OSError, match="failed to"):
|
||||
await store.add("user prefers dark mode")
|
||||
|
||||
@@ -313,7 +325,7 @@ class TestPackageExports:
|
||||
def test_lazily_exported_from_vended_memory_stores(self):
|
||||
import strands.vended_memory_stores as vended_memory_stores
|
||||
|
||||
assert vended_memory_stores.LocalMemoryStore is LocalMemoryStore
|
||||
assert vended_memory_stores.TestMemoryStore is TestMemoryStore
|
||||
|
||||
def test_unknown_attribute_raises_attribute_error(self):
|
||||
import strands.vended_memory_stores as vended_memory_stores
|
||||
@@ -80,9 +80,9 @@
|
||||
"types": "./dist/src/vended-memory-stores/bedrock-knowledge-base/index.d.ts",
|
||||
"default": "./dist/src/vended-memory-stores/bedrock-knowledge-base/index.js"
|
||||
},
|
||||
"./vended-memory-stores/local": {
|
||||
"types": "./dist/src/vended-memory-stores/local/index.d.ts",
|
||||
"default": "./dist/src/vended-memory-stores/local/index.js"
|
||||
"./vended-memory-stores/test-memory-store": {
|
||||
"types": "./dist/src/vended-memory-stores/test-memory-store/index.d.ts",
|
||||
"default": "./dist/src/vended-memory-stores/test-memory-store/index.js"
|
||||
},
|
||||
"./telemetry": {
|
||||
"types": "./dist/src/telemetry/index.d.ts",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { LocalMemoryStore, type LocalMemoryStoreConfig, type LocalMemoryAddResult } from './store.js'
|
||||
+39
-39
@@ -3,7 +3,7 @@ import { promises as fs } from 'node:fs'
|
||||
import * as os from 'node:os'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { LocalMemoryStore } from '../store.js'
|
||||
import { TestMemoryStore } from '../store.js'
|
||||
import { MemoryManager } from '../../../memory/index.js'
|
||||
import { InvocationTrigger } from '../../../memory/extraction/triggers.js'
|
||||
import type { Extractor } from '../../../memory/extraction/types.js'
|
||||
@@ -17,7 +17,7 @@ let dir: string
|
||||
let counter = 0
|
||||
|
||||
beforeEach(async () => {
|
||||
dir = join(tmpdir(), `strands-local-memory-${process.pid}-${counter++}`)
|
||||
dir = join(tmpdir(), `strands-test-memory-${process.pid}-${counter++}`)
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
})
|
||||
|
||||
@@ -29,62 +29,62 @@ function filePath(name = 'notes'): string {
|
||||
return join(dir, `${name}.json`)
|
||||
}
|
||||
|
||||
describe('LocalMemoryStore', () => {
|
||||
describe('TestMemoryStore', () => {
|
||||
describe('constructor', () => {
|
||||
it('is writable by default', () => {
|
||||
expect(new LocalMemoryStore({ name: 'notes' }).writable).toBe(true)
|
||||
expect(new TestMemoryStore({ name: 'notes' }).writable).toBe(true)
|
||||
})
|
||||
|
||||
it('honors an explicit writable: false', () => {
|
||||
expect(new LocalMemoryStore({ name: 'notes', writable: false }).writable).toBe(false)
|
||||
expect(new TestMemoryStore({ name: 'notes', writable: false }).writable).toBe(false)
|
||||
})
|
||||
|
||||
it('exposes name, description, and maxSearchResults', () => {
|
||||
const store = new LocalMemoryStore({ name: 'notes', description: 'my notes', maxSearchResults: 7 })
|
||||
const store = new TestMemoryStore({ name: 'notes', description: 'my notes', maxSearchResults: 7 })
|
||||
expect(store.name).toBe('notes')
|
||||
expect(store.description).toBe('my notes')
|
||||
expect(store.maxSearchResults).toBe(7)
|
||||
})
|
||||
|
||||
it('throws when maxSearchResults is less than 1', () => {
|
||||
expect(() => new LocalMemoryStore({ name: 'notes', maxSearchResults: 0 })).toThrow(
|
||||
expect(() => new TestMemoryStore({ name: 'notes', maxSearchResults: 0 })).toThrow(
|
||||
'maxSearchResults must be at least 1'
|
||||
)
|
||||
})
|
||||
|
||||
it('throws when name is empty', () => {
|
||||
expect(() => new LocalMemoryStore({ name: ' ' })).toThrow('name must not be empty')
|
||||
expect(() => new TestMemoryStore({ name: ' ' })).toThrow('name must not be empty')
|
||||
})
|
||||
|
||||
it('throws when an explicit path is empty', () => {
|
||||
expect(() => new LocalMemoryStore({ name: 'notes', path: ' ' })).toThrow('path must not be empty')
|
||||
expect(() => new TestMemoryStore({ name: 'notes', path: ' ' })).toThrow('path must not be empty')
|
||||
})
|
||||
|
||||
it('does no filesystem I/O on construction', async () => {
|
||||
new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
await expect(fs.access(filePath())).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('add', () => {
|
||||
it('throws when the store is not writable', async () => {
|
||||
const store = new LocalMemoryStore({ name: 'notes', writable: false, path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', writable: false, path: filePath() })
|
||||
await expect(store.add('fact')).rejects.toThrow('store is not writable')
|
||||
})
|
||||
|
||||
it('throws on empty or whitespace content', async () => {
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
await expect(store.add(' ')).rejects.toThrow('content must not be empty')
|
||||
})
|
||||
|
||||
it('returns an id for stored content', async () => {
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
const { id } = await store.add('user prefers dark mode')
|
||||
expect(id).toBeTruthy()
|
||||
})
|
||||
|
||||
it('deduplicates identical content, returning the existing id', async () => {
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
const first = await store.add('user prefers dark mode')
|
||||
const second = await store.add('user prefers dark mode')
|
||||
expect(second.id).toBe(first.id)
|
||||
@@ -93,7 +93,7 @@ describe('LocalMemoryStore', () => {
|
||||
})
|
||||
|
||||
it('persists a human-readable JSON array to disk', async () => {
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
await store.add('user prefers dark mode', { source: 'user' })
|
||||
const raw = await fs.readFile(filePath(), 'utf8')
|
||||
expect(raw).toContain('\n ') // pretty-printed
|
||||
@@ -107,19 +107,19 @@ describe('LocalMemoryStore', () => {
|
||||
|
||||
describe('search', () => {
|
||||
it('throws when maxSearchResults is less than 1', async () => {
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
await expect(store.search('q', { maxSearchResults: 0 })).rejects.toThrow('maxSearchResults must be at least 1')
|
||||
})
|
||||
|
||||
it('returns no results for an empty or token-less query', async () => {
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
await store.add('user prefers dark mode')
|
||||
expect(await store.search('')).toEqual([])
|
||||
expect(await store.search(' ... ')).toEqual([])
|
||||
})
|
||||
|
||||
it('ranks higher token overlap first and exposes _relevanceScore', async () => {
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
await store.add('the cat sat on the mat')
|
||||
await store.add('the cat chased the dog in the park')
|
||||
|
||||
@@ -131,7 +131,7 @@ describe('LocalMemoryStore', () => {
|
||||
})
|
||||
|
||||
it('excludes records with no token overlap', async () => {
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
await store.add('the cat sat on the mat')
|
||||
await store.add('a completely unrelated note')
|
||||
|
||||
@@ -141,7 +141,7 @@ describe('LocalMemoryStore', () => {
|
||||
})
|
||||
|
||||
it('breaks ties by recency (most recent first)', async () => {
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
// Stamp deterministic, increasing timestamps so the tie-break is testable.
|
||||
const isoSpy = vi
|
||||
.spyOn(Date.prototype, 'toISOString')
|
||||
@@ -157,7 +157,7 @@ describe('LocalMemoryStore', () => {
|
||||
})
|
||||
|
||||
it('caps results to maxSearchResults', async () => {
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
await store.add('alpha match')
|
||||
await store.add('beta match')
|
||||
await store.add('gamma match')
|
||||
@@ -166,7 +166,7 @@ describe('LocalMemoryStore', () => {
|
||||
})
|
||||
|
||||
it('tokenizes non-ASCII content as whole words (Unicode-aware, matching Python)', async () => {
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
await store.add('the café in 日本 is naïve')
|
||||
expect(await store.search('café')).toHaveLength(1)
|
||||
expect(await store.search('日本')).toHaveLength(1)
|
||||
@@ -175,26 +175,26 @@ describe('LocalMemoryStore', () => {
|
||||
|
||||
describe('persistence', () => {
|
||||
it('recalls entries from a fresh instance pointed at the same file (survives restart)', async () => {
|
||||
const first = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const first = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
await first.add('user lives in Berlin')
|
||||
|
||||
const second = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const second = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
const results = await second.search('where does the user live')
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0]?.content).toBe('user lives in Berlin')
|
||||
})
|
||||
|
||||
it('is ephemeral when persist is false: no file, a fresh instance forgets', async () => {
|
||||
const first = new LocalMemoryStore({ name: 'notes', persist: false, path: filePath() })
|
||||
const first = new TestMemoryStore({ name: 'notes', persist: false, path: filePath() })
|
||||
await first.add('ephemeral fact')
|
||||
await expect(fs.access(filePath())).rejects.toThrow()
|
||||
|
||||
const second = new LocalMemoryStore({ name: 'notes', persist: false, path: filePath() })
|
||||
const second = new TestMemoryStore({ name: 'notes', persist: false, path: filePath() })
|
||||
expect(await second.search('ephemeral fact')).toEqual([])
|
||||
})
|
||||
|
||||
it('starts empty when the backing file is missing', async () => {
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
expect(await store.search('anything')).toEqual([])
|
||||
})
|
||||
|
||||
@@ -206,7 +206,7 @@ describe('LocalMemoryStore', () => {
|
||||
process.env.HOME = dir
|
||||
process.env.USERPROFILE = dir
|
||||
try {
|
||||
const store = new LocalMemoryStore({ name: '../weird/name' })
|
||||
const store = new TestMemoryStore({ name: '../weird/name' })
|
||||
await store.add('a fact worth keeping')
|
||||
const expected = join(os.homedir(), '.strands', 'memory', '__weird_name.json')
|
||||
await expect(fs.access(expected)).resolves.toBeUndefined()
|
||||
@@ -218,19 +218,19 @@ describe('LocalMemoryStore', () => {
|
||||
|
||||
it('throws a clear error on a corrupt backing file', async () => {
|
||||
await fs.writeFile(filePath(), 'not json{', 'utf8')
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
await expect(store.search('anything')).rejects.toThrow('invalid JSON')
|
||||
})
|
||||
|
||||
it('throws a clear error on a valid-but-wrong-shape backing file', async () => {
|
||||
await fs.writeFile(filePath(), '{}', 'utf8')
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
await expect(store.search('anything')).rejects.toThrow('expected a JSON array')
|
||||
})
|
||||
|
||||
it('throws a clear error on a malformed record', async () => {
|
||||
await fs.writeFile(filePath(), JSON.stringify([{ foo: 'bar' }]), 'utf8')
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
await expect(store.search('anything')).rejects.toThrow('each record must have string')
|
||||
})
|
||||
|
||||
@@ -239,12 +239,12 @@ describe('LocalMemoryStore', () => {
|
||||
// wrapped "failed to read/write" error naming the path rather than a bare OS error.
|
||||
const blocker = join(dir, 'blocker')
|
||||
await fs.writeFile(blocker, 'not a directory', 'utf8')
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: join(blocker, 'notes.json') })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: join(blocker, 'notes.json') })
|
||||
await expect(store.add('user prefers dark mode')).rejects.toThrow('failed to')
|
||||
})
|
||||
|
||||
it('keeps all entries when many writes happen concurrently', async () => {
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
await Promise.all(Array.from({ length: 10 }, (_, index) => store.add(`fact number ${index}`)))
|
||||
const raw = await fs.readFile(filePath(), 'utf8')
|
||||
expect(JSON.parse(raw)).toHaveLength(10)
|
||||
@@ -254,7 +254,7 @@ describe('LocalMemoryStore', () => {
|
||||
// On a cold store, search() and add() both trigger the lazy load; the memoized load must keep
|
||||
// the added record visible to subsequent searches rather than overwriting it with a pre-write
|
||||
// snapshot.
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
await Promise.all([store.add('zebra giraffe'), store.search('apple')])
|
||||
const results = await store.search('zebra')
|
||||
expect(results).toHaveLength(1)
|
||||
@@ -276,7 +276,7 @@ describe('LocalMemoryStore', () => {
|
||||
]
|
||||
await fs.writeFile(filePath(), JSON.stringify(pyWritten), 'utf8')
|
||||
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
const results = await store.search('dark mode preference')
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0]?.content).toBe('the user prefers dark mode')
|
||||
@@ -285,7 +285,7 @@ describe('LocalMemoryStore', () => {
|
||||
})
|
||||
|
||||
it('writes the shared camelCase format with a Z-suffixed millisecond timestamp', async () => {
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
await store.add('hello world')
|
||||
const record = JSON.parse(await fs.readFile(filePath(), 'utf8'))[0]
|
||||
expect(Object.keys(record).sort()).toEqual(['content', 'createdAt', 'id'])
|
||||
@@ -295,7 +295,7 @@ describe('LocalMemoryStore', () => {
|
||||
|
||||
describe('MemoryManager integration', () => {
|
||||
it('stamps storeName on results returned through the manager', async () => {
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
await store.add('user prefers dark mode')
|
||||
|
||||
const mm = new MemoryManager({ stores: [store] })
|
||||
@@ -305,7 +305,7 @@ describe('LocalMemoryStore', () => {
|
||||
})
|
||||
|
||||
it('writes through the manager add API', async () => {
|
||||
const store = new LocalMemoryStore({ name: 'notes', path: filePath() })
|
||||
const store = new TestMemoryStore({ name: 'notes', path: filePath() })
|
||||
const mm = new MemoryManager({ stores: [store], addToolConfig: true })
|
||||
await mm.add('user likes coffee')
|
||||
expect(JSON.parse(await fs.readFile(filePath(), 'utf8'))).toHaveLength(1)
|
||||
@@ -315,7 +315,7 @@ describe('LocalMemoryStore', () => {
|
||||
const extractor: Extractor = {
|
||||
extract: vi.fn().mockResolvedValue([{ content: 'user prefers dark mode' }]),
|
||||
}
|
||||
const store = new LocalMemoryStore({
|
||||
const store = new TestMemoryStore({
|
||||
name: 'notes',
|
||||
path: filePath(),
|
||||
extraction: { trigger: new InvocationTrigger(), extractor },
|
||||
@@ -0,0 +1 @@
|
||||
export { TestMemoryStore, type TestMemoryStoreConfig, type TestMemoryAddResult } from './store.js'
|
||||
+30
-30
@@ -14,7 +14,7 @@ const RELEVANCE_SCORE_KEY = '_relevanceScore'
|
||||
/**
|
||||
* A stored memory, as it is persisted on disk.
|
||||
*/
|
||||
interface LocalMemoryRecord {
|
||||
interface TestMemoryRecord {
|
||||
id: string
|
||||
content: string
|
||||
metadata?: Record<string, JSONValue>
|
||||
@@ -22,12 +22,12 @@ interface LocalMemoryRecord {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for {@link LocalMemoryStore}.
|
||||
* Configuration for {@link TestMemoryStore}.
|
||||
*
|
||||
* The store persists to disk by default so the memory records persist across restarts. Set
|
||||
* {@link persist} to `false` for an ephemeral, single session store (useful for e.g. testing).
|
||||
*/
|
||||
export interface LocalMemoryStoreConfig extends MemoryStoreConfig {
|
||||
export interface TestMemoryStoreConfig extends MemoryStoreConfig {
|
||||
/**
|
||||
* Whether to persist entries to disk so they survive across sessions.
|
||||
* - `true` (default): writes are flushed to {@link path} (or the default location).
|
||||
@@ -43,8 +43,8 @@ export interface LocalMemoryStoreConfig extends MemoryStoreConfig {
|
||||
path?: string
|
||||
}
|
||||
|
||||
/** Result returned by {@link LocalMemoryStore.add}. */
|
||||
export interface LocalMemoryAddResult {
|
||||
/** Result returned by {@link TestMemoryStore.add}. */
|
||||
export interface TestMemoryAddResult {
|
||||
/** The id of the stored record. */
|
||||
id: string
|
||||
}
|
||||
@@ -99,22 +99,22 @@ function tokenOverlapScore(queryTokens: Set<string>, content: string): number {
|
||||
* Each {@link add} rewrites the whole file, so this fits modest volumes, not fit for high volume
|
||||
* production workloads. Use a managed store like {@link BedrockKnowledgeBaseStore} for that.
|
||||
*
|
||||
* The on-disk format is shared with the Python SDK's `LocalMemoryStore`: records use the same
|
||||
* The on-disk format is shared with the Python SDK's `TestMemoryStore`: records use the same
|
||||
* camelCase keys (`id`, `content`, `metadata`, `createdAt`) and the same timestamp shape, so a
|
||||
* backing file written by either SDK can be read by the other.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { LocalMemoryStore } from '@strands-agents/sdk/vended-memory-stores/local'
|
||||
* import { TestMemoryStore } from '@strands-agents/sdk/vended-memory-stores/test-memory-store'
|
||||
*
|
||||
* // Persists to ~/.strands/memory/notes.json by default.
|
||||
* const store = new LocalMemoryStore({ name: 'notes' })
|
||||
* const store = new TestMemoryStore({ name: 'notes' })
|
||||
*
|
||||
* const { id } = await store.add('User prefers dark mode')
|
||||
* const results = await store.search('what theme does the user like?')
|
||||
* ```
|
||||
*/
|
||||
export class LocalMemoryStore implements MemoryStore {
|
||||
export class TestMemoryStore implements MemoryStore {
|
||||
readonly name: string
|
||||
readonly description?: string
|
||||
readonly maxSearchResults?: number
|
||||
@@ -126,27 +126,27 @@ export class LocalMemoryStore implements MemoryStore {
|
||||
private readonly _explicitPath: string | undefined
|
||||
private _resolvedPath: string | undefined
|
||||
/** The loaded records once {@link _load} resolves; the working in-memory copy thereafter. */
|
||||
private _records: LocalMemoryRecord[] | undefined
|
||||
private _records: TestMemoryRecord[] | undefined
|
||||
/**
|
||||
* Memoizes the first (async) load so concurrent `search`/`add` callers share a single file read
|
||||
* instead of each racing their own — without it, a search interleaved with a first-use add could
|
||||
* overwrite the cache with a pre-write snapshot and drop the just-added record.
|
||||
*/
|
||||
private _loadPromise: Promise<LocalMemoryRecord[]> | undefined
|
||||
private _loadPromise: Promise<TestMemoryRecord[]> | undefined
|
||||
/** Serializes writes so concurrent `add`s never interleave the load-modify-flush cycle. */
|
||||
private _writeChain: Promise<unknown> = Promise.resolve()
|
||||
|
||||
constructor(options: LocalMemoryStoreConfig) {
|
||||
constructor(options: TestMemoryStoreConfig) {
|
||||
const { name, description, maxSearchResults, writable, extraction, persist, path } = options
|
||||
|
||||
if (!name.trim()) {
|
||||
throw new Error('LocalMemoryStore: name must not be empty.')
|
||||
throw new Error('TestMemoryStore: name must not be empty.')
|
||||
}
|
||||
this.name = name
|
||||
if (description !== undefined) this.description = description
|
||||
if (maxSearchResults !== undefined) {
|
||||
if (maxSearchResults < 1) {
|
||||
throw new Error('LocalMemoryStore: maxSearchResults must be at least 1.')
|
||||
throw new Error('TestMemoryStore: maxSearchResults must be at least 1.')
|
||||
}
|
||||
this.maxSearchResults = maxSearchResults
|
||||
}
|
||||
@@ -155,7 +155,7 @@ export class LocalMemoryStore implements MemoryStore {
|
||||
if (extraction !== undefined) this.extraction = extraction
|
||||
|
||||
if (path !== undefined && !path.trim()) {
|
||||
throw new Error('LocalMemoryStore: path must not be empty.')
|
||||
throw new Error('TestMemoryStore: path must not be empty.')
|
||||
}
|
||||
this._persist = persist ?? true
|
||||
this._explicitPath = path
|
||||
@@ -173,7 +173,7 @@ export class LocalMemoryStore implements MemoryStore {
|
||||
*/
|
||||
async search(query: string, options?: SearchOptions): Promise<MemoryEntry[]> {
|
||||
if (options?.maxSearchResults !== undefined && options.maxSearchResults < 1) {
|
||||
throw new Error('LocalMemoryStore: maxSearchResults must be at least 1.')
|
||||
throw new Error('TestMemoryStore: maxSearchResults must be at least 1.')
|
||||
}
|
||||
const limit = options?.maxSearchResults || this.maxSearchResults || DEFAULT_MAX_SEARCH_RESULTS
|
||||
|
||||
@@ -182,7 +182,7 @@ export class LocalMemoryStore implements MemoryStore {
|
||||
|
||||
const records = await this._load()
|
||||
|
||||
const scored: Array<{ record: LocalMemoryRecord; score: number }> = []
|
||||
const scored: Array<{ record: TestMemoryRecord; score: number }> = []
|
||||
for (const record of records) {
|
||||
const score = tokenOverlapScore(queryTokens, record.content)
|
||||
if (score > 0) scored.push({ record, score })
|
||||
@@ -209,12 +209,12 @@ export class LocalMemoryStore implements MemoryStore {
|
||||
* overwritten in search output.
|
||||
* @returns The id of the stored (or already-present) record
|
||||
*/
|
||||
async add(content: string, metadata?: Record<string, JSONValue>): Promise<LocalMemoryAddResult> {
|
||||
async add(content: string, metadata?: Record<string, JSONValue>): Promise<TestMemoryAddResult> {
|
||||
if (!this.writable) {
|
||||
throw new Error('LocalMemoryStore: store is not writable. Set writable: true in config to enable add().')
|
||||
throw new Error('TestMemoryStore: store is not writable. Set writable: true in config to enable add().')
|
||||
}
|
||||
if (!content.trim()) {
|
||||
throw new Error('LocalMemoryStore: content must not be empty.')
|
||||
throw new Error('TestMemoryStore: content must not be empty.')
|
||||
}
|
||||
|
||||
// Serialize the whole load-modify-flush cycle behind any in-flight write so concurrent `add`s
|
||||
@@ -226,7 +226,7 @@ export class LocalMemoryStore implements MemoryStore {
|
||||
const existing = records.find((record) => record.content.trim() === normalizedContent)
|
||||
if (existing) return { id: existing.id }
|
||||
|
||||
const record: LocalMemoryRecord = { id: uuidv7(), content, createdAt: new Date().toISOString() }
|
||||
const record: TestMemoryRecord = { id: uuidv7(), content, createdAt: new Date().toISOString() }
|
||||
if (metadata !== undefined) record.metadata = metadata
|
||||
|
||||
// Flush the candidate list first and only commit it to the in-memory cache once the write
|
||||
@@ -267,7 +267,7 @@ export class LocalMemoryStore implements MemoryStore {
|
||||
* first call's promise is memoized in {@link _loadPromise} so concurrent callers await one shared
|
||||
* read rather than each loading independently and racing to assign the cache.
|
||||
*/
|
||||
private async _load(): Promise<LocalMemoryRecord[]> {
|
||||
private async _load(): Promise<TestMemoryRecord[]> {
|
||||
if (this._records !== undefined) return this._records
|
||||
if (this._loadPromise !== undefined) return this._loadPromise
|
||||
|
||||
@@ -281,7 +281,7 @@ export class LocalMemoryStore implements MemoryStore {
|
||||
}
|
||||
|
||||
/** Reads and parses the backing file (or returns an empty list when ephemeral / missing). */
|
||||
private async _readFromDisk(): Promise<LocalMemoryRecord[]> {
|
||||
private async _readFromDisk(): Promise<TestMemoryRecord[]> {
|
||||
const filePath = await this._getPath()
|
||||
if (filePath === undefined) return []
|
||||
|
||||
@@ -291,17 +291,17 @@ export class LocalMemoryStore implements MemoryStore {
|
||||
rawContent = await readFile(filePath, 'utf8')
|
||||
} catch (error: unknown) {
|
||||
if ((error as { code?: string }).code === 'ENOENT') return []
|
||||
throw new Error(`LocalMemoryStore: failed to read ${filePath}`, { cause: error })
|
||||
throw new Error(`TestMemoryStore: failed to read ${filePath}`, { cause: error })
|
||||
}
|
||||
|
||||
let parsedFile: unknown
|
||||
try {
|
||||
parsedFile = JSON.parse(rawContent)
|
||||
} catch (error: unknown) {
|
||||
throw new Error(`LocalMemoryStore: invalid JSON in ${filePath}`, { cause: error })
|
||||
throw new Error(`TestMemoryStore: invalid JSON in ${filePath}`, { cause: error })
|
||||
}
|
||||
if (!Array.isArray(parsedFile)) {
|
||||
throw new Error(`LocalMemoryStore: invalid backing file ${filePath}: expected a JSON array of records`)
|
||||
throw new Error(`TestMemoryStore: invalid backing file ${filePath}: expected a JSON array of records`)
|
||||
}
|
||||
for (const record of parsedFile) {
|
||||
if (
|
||||
@@ -312,12 +312,12 @@ export class LocalMemoryStore implements MemoryStore {
|
||||
typeof record.createdAt !== 'string'
|
||||
) {
|
||||
throw new Error(
|
||||
`LocalMemoryStore: invalid backing file ${filePath}: ` +
|
||||
`TestMemoryStore: invalid backing file ${filePath}: ` +
|
||||
"each record must have string 'id', 'content', and 'createdAt' fields"
|
||||
)
|
||||
}
|
||||
}
|
||||
return parsedFile as LocalMemoryRecord[]
|
||||
return parsedFile as TestMemoryRecord[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -326,7 +326,7 @@ export class LocalMemoryStore implements MemoryStore {
|
||||
* serialize invocations via {@link _writeChain}. Throws with the target path (and the OS error as
|
||||
* `cause`) when the path is unreachable or not writable.
|
||||
*/
|
||||
private async _flush(records: LocalMemoryRecord[]): Promise<void> {
|
||||
private async _flush(records: TestMemoryRecord[]): Promise<void> {
|
||||
const filePath = await this._getPath()
|
||||
if (filePath === undefined) return
|
||||
|
||||
@@ -338,7 +338,7 @@ export class LocalMemoryStore implements MemoryStore {
|
||||
await writeFile(tmpPath, JSON.stringify(records, null, 2), 'utf8')
|
||||
await rename(tmpPath, filePath)
|
||||
} catch (error: unknown) {
|
||||
throw new Error(`LocalMemoryStore: failed to write ${filePath}`, { cause: error })
|
||||
throw new Error(`TestMemoryStore: failed to write ${filePath}`, { cause: error })
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user