ReMe/tests4/unit/test_write_metadata_lock.py
jinliyl d8086039dc
Some checks are pending
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Tests ReMe / Unit Tests - py3.11 (push) Waiting to run
Tests ReMe / Unit Tests - py3.12 (push) Waiting to run
Tests ReMe / Unit Tests - py3.13 (push) Waiting to run
refactor(Agentscope2.0): llm & embedding & agent (#271)
* refactor(embedding): replace embedding model with embedding store architecture

- Remove as_token_counter component and its estimated token counter implementation
- Replace BaseEmbeddingModel with BaseEmbedding that wraps AgentScope embedding models
- Add support for multiple embedding providers (OpenAI, DashScope, Gemini, Ollama)
- Introduce BaseEmbeddingStore and LocalEmbeddingStore for caching and persistence
- Update component registry to use new embedding and embedding_store types
- Modify file stores to use embedding_store instead of embedding_model
- Update health check to monitor embedding_store instead of embedding_model
- Change default config to use embedding_store with local backend
- Add estimate_token_count utility function to utils module

* refactor(llm): replace as_llm components with unified llm implementation

- Remove deprecated as_llm and as_llm_formatter modules
- Add new llm module with BaseLLM and provider-specific implementations
- Update component registry to use LLM instead of AS_LLM
- Replace all as_llm/as_llm_formatter references with llm in steps
- Update configuration schema to use llm instead of as_llm
- Rename integration test file from test_as_llm to test_llm
- Add proper docstrings to embedding store dimension property
- Add pylint disable comment for embedding model call
- Remove unused FormatterBase import in base_step
- Update token_utils with function docstring

* refactor(evolve): replace ReActAgent with Agent and update message handling

- Removed FlexReActAgent class and direct ReActAgent imports
- Updated Agent instantiation to use new constructor parameters
- Changed message content to use TextBlock format instead of plain strings
- Modified timestamp access from msg.timestamp to msg.created_at
- Updated metadata access pattern for structured outputs
- Replaced Msg.from_dict with Msg.model_validate in auto_memory.py
- Updated test mocks to patch Agent instead of ReActAgent
- Changed message serialization from to_dict to model_dump in tests
- Moved component references to base class definition
- Updated demo tools to return strings instead of ToolResponse objects

* feat(step): migrate to FunctionTool and add streaming support

- Replace deprecated ToolResponse with FunctionTool in base_step.py
- Remove unused TextBlock import from base_step.py
- Update job registration to use new FunctionTool API
- Add thinking_budget parameter to llm_demo configuration
- Introduce StreamLLMDemoStep with streaming output capability
- Add structured output support to LLMDemoStep via generate_structured_output
- Implement streaming event handling for text/thinking/tool calls
- Add integration tests for embedding functionality
- Add integration tests for structured output and streaming features
- Update tool usage in demo steps to use new function naming convention

* fix(ci): correct package installation path in unittest workflow

- Updated pip install command to use proper package path "./reme4[dev,core]"
- Fixed dependency installation step in CI workflow configuration

* chore(workflow): update python versions in unittest workflow

- Remove Python 3.10 from test matrix
- Add Python 3.11 to test matrix
- Add Python 3.12 to test matrix
- Keep Python 3.13 in test matrix
- Update matrix configuration for better version coverage

* fix(health): handle missing dimensions attribute in embedding status

- Wrap dimensions access in try-except to prevent AttributeError
- Return None when dimensions attribute is not available
- Maintain backward compatibility for components without dimensions

test(component): add comprehensive tests for BaseComponent and related classes

- Add tests for Dependency class including repr and attribute access
- Add tests for bind method with various scenarios and edge cases
- Add tests for lifecycle management and async context handling
- Add tests for standalone and context-bound dependency resolution
- Add tests for ComponentMixin path utilities

test(common): update LocalFileStore initialization parameter

- Change embedding_model parameter to embedding_store in test setup
- Update all affected test files consistently

test(registry): add complete test suite for ComponentRegistry

- Add tests for register method with explicit names and defaults
- Add tests for decorator registration pattern
- Add tests for get_all method returning copies
- Add tests for unregister and clear operations
- Add tests for error handling of invalid registrations

test(job): add comprehensive tests for BaseJob and BackgroundJob

- Add tests for step resolution and exception handling
- Add tests for backoff delay calculation with jitter
- Add tests for supervisor loop restart behavior
- Add tests for task shutdown and cancellation

test(prompt): add complete test suite for PromptHandler

- Add tests for prompt loading from dictionaries and files
- Add tests for internationalization and language fallback
- Add tests for flag filtering and variable substitution
- Add tests for format validation and error handling

test(runtime): add basic tests for RuntimeContext dictionary access

- Add tests for item getting, setting and containment checks
- Add tests for missing key error handling

* feat(evolve): add permission context and agent state management

- Import PermissionContext, PermissionMode and AgentState modules
- Add state configuration with bypass permission mode to AutoDream agents
- Add state configuration with bypass permission mode to AutoMemory agents
- Implement static _to_msg method for message validation and formatting
- Refactor message processing to use the new _to_msg method
- Ensure proper content structure for text blocks in message conversion

* style(tests): update test files with linting rules and code improvements

- Add missing pylint disable directives for docstring and attribute warnings
- Replace lambda expressions with proper function definitions in test cases
- Import Path directly instead of using lambda with __import__
- Simplify assertion checks by using truthiness instead of equality to empty dict
- Remove unused imports and reorder imports consistently
- Format dictionary literals with proper indentation and line breaks
2026-06-03 11:35:43 +08:00

207 lines
7.3 KiB
Python

"""Tests for the WriteStep ``metadata`` param and the per-path write lock.
The ``metadata`` feature lets callers extend the on-disk frontmatter beyond
the two reserved fields (``name`` / ``description``) without touching the
step interface. Reserved keys inside the dict are ignored — explicit
top-level parameters always win.
The per-path lock serializes concurrent write_step invocations targeting
the same path within a single process. We exercise it by firing many
concurrent writes at one path and asserting the final state is consistent
(the lock guarantees the last write's bytes land intact, not a torn
interleaving).
"""
# pylint: disable=protected-access
import asyncio
import os
import tempfile
import warnings
from pathlib import Path
import frontmatter
from reme4.components.file_store import LocalFileStore
from reme4.steps.file_io import write as crud_write
warnings.filterwarnings("ignore", category=DeprecationWarning, module="jieba")
warnings.filterwarnings("ignore", category=DeprecationWarning, module="pkg_resources")
class temp_chdir:
"""Context manager to temporarily chdir into a path and restore on exit."""
def __init__(self, path):
self.path = path
self.old = None
def __enter__(self):
self.old = os.getcwd()
os.chdir(self.path)
return self
def __exit__(self, *exc):
os.chdir(self.old)
def _run(coro):
asyncio.run(coro)
async def _make_store() -> LocalFileStore:
store = LocalFileStore(name="t_write_meta", embedding_store="")
await store.start()
return store
async def _write(store: LocalFileStore, **kwargs):
step = crud_write.WriteStep(file_store=store)
await step(**kwargs)
return step.context.response
# -- metadata expansion ------------------------------------------------------
def test_write_metadata_extends_frontmatter():
"""``metadata={"tags": [...]}`` ends up as a frontmatter field on disk."""
async def run():
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
store = await _make_store()
resp = await _write(
store,
path="note.md",
name="Hello",
description="A note",
content="body text",
metadata={"tags": ["alpha", "beta"], "priority": 3},
)
assert resp.success is True, resp
on_disk = (Path(tmp) / "note.md").read_text(encoding="utf-8")
post = frontmatter.loads(on_disk)
assert post.metadata["name"] == "Hello"
assert post.metadata["description"] == "A note"
assert post.metadata["tags"] == ["alpha", "beta"]
assert post.metadata["priority"] == 3
assert post.content.strip() == "body text"
await store.close()
print("✓ test_write_metadata_extends_frontmatter passed")
_run(run())
def test_write_metadata_reserved_keys_ignored():
"""``name`` / ``description`` inside ``metadata`` are dropped; explicit args win."""
async def run():
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
store = await _make_store()
resp = await _write(
store,
path="note.md",
name="ExplicitName",
description="ExplicitDesc",
content="body",
metadata={"name": "DroppedName", "description": "DroppedDesc", "tag": "kept"},
)
assert resp.success is True, resp
post = frontmatter.loads((Path(tmp) / "note.md").read_text(encoding="utf-8"))
assert post.metadata["name"] == "ExplicitName"
assert post.metadata["description"] == "ExplicitDesc"
assert post.metadata["tag"] == "kept"
await store.close()
print("✓ test_write_metadata_reserved_keys_ignored passed")
_run(run())
def test_write_no_metadata_preserves_legacy_shape():
"""No ``metadata`` arg → frontmatter still contains only name/description."""
async def run():
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
store = await _make_store()
resp = await _write(
store,
path="note.md",
name="N",
description="D",
content="body",
)
assert resp.success is True, resp
post = frontmatter.loads((Path(tmp) / "note.md").read_text(encoding="utf-8"))
assert set(post.metadata.keys()) == {"name", "description"}
await store.close()
print("✓ test_write_no_metadata_preserves_legacy_shape passed")
_run(run())
def test_write_non_md_drops_metadata():
"""Non-markdown target: metadata silently dropped, body written verbatim."""
async def run():
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
store = await _make_store()
resp = await _write(
store,
path="note.txt",
name="N",
description="D",
content="plain body",
metadata={"tag": "ignored"},
)
assert resp.success is True, resp
on_disk = (Path(tmp) / "note.txt").read_text(encoding="utf-8")
assert on_disk == "plain body"
await store.close()
print("✓ test_write_non_md_drops_metadata passed")
_run(run())
# -- per-path lock -----------------------------------------------------------
def test_write_lock_serializes_concurrent_writes():
"""Many concurrent writes at one path land cleanly — no torn frontmatter.
Without the lock, concurrent writers can interleave reads-of-existence
and writes-of-bytes; with it, each write either runs before or after
every other write. The on-disk file at the end must parse as valid
frontmatter with name matching exactly one of the writers.
"""
async def run():
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
store = await _make_store()
n = 16
await asyncio.gather(
*(
_write(
store,
path="shared.md",
name=f"writer-{i}",
description=f"desc-{i}",
content=f"body-{i}",
)
for i in range(n)
),
)
on_disk = (Path(tmp) / "shared.md").read_text(encoding="utf-8")
post = frontmatter.loads(on_disk)
assert post.metadata.get("name", "").startswith("writer-"), post.metadata
assert post.metadata.get("description", "").startswith("desc-"), post.metadata
assert post.content.strip().startswith("body-"), post.content
# The body, name, and description must all come from the SAME write
# (no interleaving). Extract the index from each.
idx_name = post.metadata["name"].split("-", 1)[1]
idx_desc = post.metadata["description"].split("-", 1)[1]
idx_body = post.content.strip().split("-", 1)[1]
assert idx_name == idx_desc == idx_body, (idx_name, idx_desc, idx_body)
await store.close()
print("✓ test_write_lock_serializes_concurrent_writes passed")
_run(run())