Files
wehub-resource-sync c3bf08ac8d
K8s Workspace Integration Tests / k8s-workspace-tests (push) Waiting to run
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Python Unittest Coverage / test (macos-15, 3.11) (push) Waiting to run
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Waiting to run
Python Unittest Coverage / test (windows-latest, 3.11) (push) Waiting to run
Web UI / check (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:39:27 +08:00

433 lines
15 KiB
Python

# -*- coding: utf-8 -*-
"""File cache test case for Read/Write/Edit tools."""
import os
import tempfile
from unittest.async_case import IsolatedAsyncioTestCase
from agentscope.state import AgentState
from agentscope.tool import Read, Write, Edit
class FileCacheTest(IsolatedAsyncioTestCase):
"""Test file cache functionality for Read/Write/Edit tools."""
async def asyncSetUp(self) -> None:
"""The async setup method."""
self.read_tool = Read()
self.write_tool = Write()
self.edit_tool = Edit()
self.state = AgentState()
# Create a temporary directory
self.temp_dir = tempfile.mkdtemp()
self.test_file = os.path.join(self.temp_dir, "test.txt")
async def asyncTearDown(self) -> None:
"""Clean up temporary files."""
import shutil
if os.path.exists(self.temp_dir):
shutil.rmtree(self.temp_dir)
async def test_edit_without_read(self) -> None:
"""Test Edit fails when file not read first."""
# Create a file
with open(self.test_file, "w", encoding="utf-8") as f:
f.write("Hello World\n")
# Try to edit without reading first
chunk = await self.edit_tool(
file_path=self.test_file,
old_string="Hello",
new_string="Hi",
_agent_state=self.state,
)
# Should fail with error
self.assertEqual(chunk.state, "error")
self.assertIn("must first read", chunk.content[0].text)
async def test_write_without_read(self) -> None:
"""Test Write fails when existing file not read first."""
# Create a file
with open(self.test_file, "w", encoding="utf-8") as f:
f.write("Existing content\n")
# Try to write without reading first
chunk = await self.write_tool(
file_path=self.test_file,
content="New content\n",
_agent_state=self.state,
)
# Should fail with error
self.assertEqual(chunk.state, "error")
self.assertIn("has not been read yet", chunk.content[0].text)
async def test_write_new_file_without_read(self) -> None:
"""Test Write succeeds for new file without reading."""
new_file = os.path.join(self.temp_dir, "new_file.txt")
# Write to a new file (doesn't exist yet)
chunk = await self.write_tool(
file_path=new_file,
content="New file content\n",
_agent_state=self.state,
)
# Should succeed
self.assertEqual(chunk.state, "running")
self.assertTrue(os.path.exists(new_file))
with open(new_file, "r", encoding="utf-8") as f:
content = f.read()
self.assertEqual(content, "New file content\n")
async def test_edit_after_read(self) -> None:
"""Test Edit succeeds after reading file."""
# Create a file
with open(self.test_file, "w", encoding="utf-8") as f:
f.write("Hello World\n")
# Read the file first
read_chunk = await self.read_tool(
file_path=self.test_file,
_agent_state=self.state,
)
self.assertEqual(read_chunk.state, "running")
# Now edit should succeed
edit_chunk = await self.edit_tool(
file_path=self.test_file,
old_string="Hello",
new_string="Hi",
_agent_state=self.state,
)
self.assertEqual(edit_chunk.state, "running")
# Verify file was edited
with open(self.test_file, "r", encoding="utf-8") as f:
content = f.read()
self.assertEqual(content, "Hi World\n")
async def test_write_after_read(self) -> None:
"""Test Write succeeds after reading file."""
# Create a file
with open(self.test_file, "w", encoding="utf-8") as f:
f.write("Old content\n")
# Read the file first
read_chunk = await self.read_tool(
file_path=self.test_file,
_agent_state=self.state,
)
self.assertEqual(read_chunk.state, "running")
# Now write should succeed
write_chunk = await self.write_tool(
file_path=self.test_file,
content="New content\n",
_agent_state=self.state,
)
self.assertEqual(write_chunk.state, "running")
# Verify file was overwritten
with open(self.test_file, "r", encoding="utf-8") as f:
content = f.read()
self.assertEqual(content, "New content\n")
async def test_cache_invalidation_after_file_deletion(self) -> None:
"""Test cache handles file deletion gracefully."""
# Create a file
with open(self.test_file, "w", encoding="utf-8") as f:
f.write("Test content\n")
# Read the file to cache it
read_chunk = await self.read_tool(
file_path=self.test_file,
_agent_state=self.state,
)
self.assertEqual(read_chunk.state, "running")
# Verify cache exists
self.assertEqual(len(self.state.tool_context.read_file_cache), 1)
self.assertEqual(
self.state.tool_context.read_file_cache[0].file_path,
self.test_file,
)
# Delete the file
os.unlink(self.test_file)
# Try to edit - should fail with "File not found" error
edit_chunk = await self.edit_tool(
file_path=self.test_file,
old_string="Test",
new_string="New",
_agent_state=self.state,
)
# Should fail with "File not found" error
self.assertEqual(edit_chunk.state, "error")
self.assertIn("not found", edit_chunk.content[0].text.lower())
# Try to read again - should also fail
read_chunk2 = await self.read_tool(
file_path=self.test_file,
_agent_state=self.state,
)
self.assertEqual(read_chunk2.state, "error")
self.assertIn("does not exist", read_chunk2.content[0].text)
async def test_cache_invalidation_after_file_modification(self) -> None:
"""Test cache detects file modification."""
# Create a file
with open(self.test_file, "w", encoding="utf-8") as f:
f.write("Original content\n")
# Read the file to cache it
read_chunk = await self.read_tool(
file_path=self.test_file,
_agent_state=self.state,
)
self.assertEqual(read_chunk.state, "running")
# Modify the file externally
import time
time.sleep(0.1) # Ensure mtime changes
with open(self.test_file, "w", encoding="utf-8") as f:
f.write("Modified content\n")
# Try to edit - should fail because cache is stale
edit_chunk = await self.edit_tool(
file_path=self.test_file,
old_string="Original",
new_string="New",
_agent_state=self.state,
)
# Should fail with error
self.assertEqual(edit_chunk.state, "error")
self.assertIn("must first read", edit_chunk.content[0].text)
async def test_cache_lru_eviction(self) -> None:
"""Test LRU cache eviction when max_cache_files is exceeded."""
# Set a small cache limit
self.state.tool_context.max_cache_files = 3
# Create and read 4 files
files = []
for i in range(4):
file_path = os.path.join(self.temp_dir, f"file{i}.txt")
with open(file_path, "w", encoding="utf-8") as f:
f.write(f"Content {i}\n")
files.append(file_path)
# Read each file
await self.read_tool(
file_path=file_path,
_agent_state=self.state,
)
# Cache should only have 3 files (oldest evicted)
self.assertEqual(len(self.state.tool_context.read_file_cache), 3)
# The first file should have been evicted
cached_paths = [
entry.file_path
for entry in self.state.tool_context.read_file_cache
]
self.assertNotIn(files[0], cached_paths)
self.assertIn(files[1], cached_paths)
self.assertIn(files[2], cached_paths)
self.assertIn(files[3], cached_paths)
async def test_cache_without_state(self) -> None:
"""Test tools work without state (fallback mode)."""
# Create a file
with open(self.test_file, "w", encoding="utf-8") as f:
f.write("Hello World\n")
# Edit without state should work (fallback to reading from disk)
edit_chunk = await self.edit_tool(
file_path=self.test_file,
old_string="Hello",
new_string="Hi",
_agent_state=None,
)
self.assertEqual(edit_chunk.state, "running")
# Verify file was edited
with open(self.test_file, "r", encoding="utf-8") as f:
content = f.read()
self.assertEqual(content, "Hi World\n")
async def test_multiple_reads_update_cache(self) -> None:
"""Test reading same file multiple times updates cache."""
# Create a file
with open(self.test_file, "w", encoding="utf-8") as f:
f.write("Content v1\n")
# Read the file
await self.read_tool(
file_path=self.test_file,
_agent_state=self.state,
)
self.assertEqual(len(self.state.tool_context.read_file_cache), 1)
# Read again - should update cache, not add duplicate
await self.read_tool(
file_path=self.test_file,
_agent_state=self.state,
)
self.assertEqual(len(self.state.tool_context.read_file_cache), 1)
async def test_read_cache_lines_contain_newlines(self) -> None:
"""Test that cached lines from Read retain trailing newlines.
readlines() includes the newline character in each line. The cache
must store them as-is so that "".join(lines) reconstructs the exact
original file content.
"""
with open(self.test_file, "w", encoding="utf-8") as f:
f.write("line1\nline2\nline3\n")
await self.read_tool(
file_path=self.test_file,
_agent_state=self.state,
)
cache = await self.state.tool_context.get_cache(self.test_file)
self.assertIsNotNone(cache)
# Each line from readlines() ends with \n
self.assertEqual(cache.lines, ["line1\n", "line2\n", "line3\n"])
# "".join reconstructs the exact original content
self.assertEqual("".join(cache.lines), "line1\nline2\nline3\n")
async def test_edit_multiline_match_from_cache(self) -> None:
"""Test Edit correctly matches multi-line old_string from
cached content.
Regression test for the bug where "\n".join(cache.lines) doubled the
newlines (e.g. "line1\n\nline2\n" instead of "line1\nline2\n"),
making multi-line old_string matching fail.
"""
with open(self.test_file, "w", encoding="utf-8") as f:
f.write("line1\nline2\nline3\n")
await self.read_tool(
file_path=self.test_file,
_agent_state=self.state,
)
# This multi-line old_string must match exactly in the reconstructed
# content; with the bug it would not be found.
chunk = await self.edit_tool(
file_path=self.test_file,
old_string="line1\nline2",
new_string="replaced",
_agent_state=self.state,
)
self.assertEqual(chunk.state, "running")
with open(self.test_file, "r", encoding="utf-8") as f:
content = f.read()
self.assertEqual(content, "replaced\nline3\n")
async def test_edit_single_line_match_from_cache(self) -> None:
"""Test Edit correctly matches single-line old_string from cache."""
with open(self.test_file, "w", encoding="utf-8") as f:
f.write("Hello World\nThis is a test\n")
await self.read_tool(
file_path=self.test_file,
_agent_state=self.state,
)
chunk = await self.edit_tool(
file_path=self.test_file,
old_string="Hello World",
new_string="Hello Python",
_agent_state=self.state,
)
self.assertEqual(chunk.state, "running")
with open(self.test_file, "r", encoding="utf-8") as f:
content = f.read()
self.assertEqual(content, "Hello Python\nThis is a test\n")
async def test_write_invalidates_cache(self) -> None:
"""Test that Write updates the cache so Edit can use it afterwards."""
with open(self.test_file, "w", encoding="utf-8") as f:
f.write("original content\n")
# Read to populate cache
await self.read_tool(
file_path=self.test_file,
_agent_state=self.state,
)
# Overwrite with Write (mtime changes, old cache becomes stale)
write_chunk = await self.write_tool(
file_path=self.test_file,
content="new content\n",
_agent_state=self.state,
)
self.assertEqual(write_chunk.state, "running")
# The old cache entry is now stale; Edit should require a new Read
edit_chunk = await self.edit_tool(
file_path=self.test_file,
old_string="new content",
new_string="updated content",
_agent_state=self.state,
)
self.assertEqual(edit_chunk.state, "error")
self.assertIn("must first read", edit_chunk.content[0].text)
async def test_write_cache_stale_then_reread(self) -> None:
"""Test workflow: Read -> Write -> Read -> Edit works correctly."""
with open(self.test_file, "w", encoding="utf-8") as f:
f.write("original\n")
# First read
await self.read_tool(
file_path=self.test_file,
_agent_state=self.state,
)
# Overwrite
import time
time.sleep(0.01) # ensure mtime changes
await self.write_tool(
file_path=self.test_file,
content="rewritten\n",
_agent_state=self.state,
)
# Re-read to refresh cache
await self.read_tool(
file_path=self.test_file,
_agent_state=self.state,
)
# Now Edit should succeed against the new content
edit_chunk = await self.edit_tool(
file_path=self.test_file,
old_string="rewritten",
new_string="final",
_agent_state=self.state,
)
self.assertEqual(edit_chunk.state, "running")
with open(self.test_file, "r", encoding="utf-8") as f:
content = f.read()
self.assertEqual(content, "final\n")