mirror of
https://github.com/mruwnik/memory.git
synced 2025-07-30 06:36:07 +02:00
synch notes
This commit is contained in:
parent
387bd962e6
commit
288c2995e5
@ -15,6 +15,7 @@ OBSERVATIONS_ROOT = "memory.workers.tasks.observations"
|
|||||||
SYNC_NOTES = f"{NOTES_ROOT}.sync_notes"
|
SYNC_NOTES = f"{NOTES_ROOT}.sync_notes"
|
||||||
SYNC_NOTE = f"{NOTES_ROOT}.sync_note"
|
SYNC_NOTE = f"{NOTES_ROOT}.sync_note"
|
||||||
SETUP_GIT_NOTES = f"{NOTES_ROOT}.setup_git_notes"
|
SETUP_GIT_NOTES = f"{NOTES_ROOT}.setup_git_notes"
|
||||||
|
TRACK_GIT_CHANGES = f"{NOTES_ROOT}.track_git_changes"
|
||||||
SYNC_OBSERVATION = f"{OBSERVATIONS_ROOT}.sync_observation"
|
SYNC_OBSERVATION = f"{OBSERVATIONS_ROOT}.sync_observation"
|
||||||
SYNC_ALL_COMICS = f"{COMIC_ROOT}.sync_all_comics"
|
SYNC_ALL_COMICS = f"{COMIC_ROOT}.sync_all_comics"
|
||||||
SYNC_SMBC = f"{COMIC_ROOT}.sync_smbc"
|
SYNC_SMBC = f"{COMIC_ROOT}.sync_smbc"
|
||||||
|
@ -105,6 +105,7 @@ COMIC_SYNC_INTERVAL = int(os.getenv("COMIC_SYNC_INTERVAL", 60 * 60 * 24))
|
|||||||
ARTICLE_FEED_SYNC_INTERVAL = int(os.getenv("ARTICLE_FEED_SYNC_INTERVAL", 30 * 60))
|
ARTICLE_FEED_SYNC_INTERVAL = int(os.getenv("ARTICLE_FEED_SYNC_INTERVAL", 30 * 60))
|
||||||
CLEAN_COLLECTION_INTERVAL = int(os.getenv("CLEAN_COLLECTION_INTERVAL", 24 * 60 * 60))
|
CLEAN_COLLECTION_INTERVAL = int(os.getenv("CLEAN_COLLECTION_INTERVAL", 24 * 60 * 60))
|
||||||
CHUNK_REINGEST_INTERVAL = int(os.getenv("CHUNK_REINGEST_INTERVAL", 60 * 60))
|
CHUNK_REINGEST_INTERVAL = int(os.getenv("CHUNK_REINGEST_INTERVAL", 60 * 60))
|
||||||
|
NOTES_SYNC_INTERVAL = int(os.getenv("NOTES_SYNC_INTERVAL", 15))
|
||||||
|
|
||||||
CHUNK_REINGEST_SINCE_MINUTES = int(os.getenv("CHUNK_REINGEST_SINCE_MINUTES", 60 * 24))
|
CHUNK_REINGEST_SINCE_MINUTES = int(os.getenv("CHUNK_REINGEST_SINCE_MINUTES", 60 * 24))
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from memory.common.celery_app import (
|
|||||||
REINGEST_MISSING_CHUNKS,
|
REINGEST_MISSING_CHUNKS,
|
||||||
SYNC_ALL_COMICS,
|
SYNC_ALL_COMICS,
|
||||||
SYNC_ALL_ARTICLE_FEEDS,
|
SYNC_ALL_ARTICLE_FEEDS,
|
||||||
|
TRACK_GIT_CHANGES,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -33,4 +34,8 @@ app.conf.beat_schedule = {
|
|||||||
"task": SYNC_ALL_ARTICLE_FEEDS,
|
"task": SYNC_ALL_ARTICLE_FEEDS,
|
||||||
"schedule": settings.ARTICLE_FEED_SYNC_INTERVAL,
|
"schedule": settings.ARTICLE_FEED_SYNC_INTERVAL,
|
||||||
},
|
},
|
||||||
|
"sync-notes-changes": {
|
||||||
|
"task": TRACK_GIT_CHANGES,
|
||||||
|
"schedule": settings.NOTES_SYNC_INTERVAL,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,13 @@ import shlex
|
|||||||
from memory.common import settings
|
from memory.common import settings
|
||||||
from memory.common.db.connection import make_session
|
from memory.common.db.connection import make_session
|
||||||
from memory.common.db.models import Note
|
from memory.common.db.models import Note
|
||||||
from memory.common.celery_app import app, SYNC_NOTE, SYNC_NOTES, SETUP_GIT_NOTES
|
from memory.common.celery_app import (
|
||||||
|
app,
|
||||||
|
SYNC_NOTE,
|
||||||
|
SYNC_NOTES,
|
||||||
|
SETUP_GIT_NOTES,
|
||||||
|
TRACK_GIT_CHANGES,
|
||||||
|
)
|
||||||
from memory.workers.tasks.content_processing import (
|
from memory.workers.tasks.content_processing import (
|
||||||
check_content_exists,
|
check_content_exists,
|
||||||
create_content_hash,
|
create_content_hash,
|
||||||
@ -41,6 +47,22 @@ def git_command(repo_root: pathlib.Path, *args: str, force: bool = False):
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def check_git_command(repo_root: pathlib.Path, *args: str, force: bool = False):
|
||||||
|
res = git_command(repo_root, *args, force=force)
|
||||||
|
if not res:
|
||||||
|
raise RuntimeError(f"`{' '.join(args)}` failed")
|
||||||
|
|
||||||
|
if res.returncode != 0:
|
||||||
|
logger.error(f"Git command failed: {res.returncode}")
|
||||||
|
logger.error(f"stderr: {res.stderr}")
|
||||||
|
if res.stdout:
|
||||||
|
logger.error(f"stdout: {res.stdout}")
|
||||||
|
raise RuntimeError(
|
||||||
|
f"`{' '.join(args)}` failed with return code {res.returncode}"
|
||||||
|
)
|
||||||
|
return res.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def git_tracking(repo_root: pathlib.Path, commit_message: str = "Sync note"):
|
def git_tracking(repo_root: pathlib.Path, commit_message: str = "Sync note"):
|
||||||
git_command(repo_root, "fetch")
|
git_command(repo_root, "fetch")
|
||||||
@ -148,3 +170,61 @@ def setup_git_notes(origin: str, email: str, name: str):
|
|||||||
git_command(settings.NOTES_STORAGE_DIR, "commit", "-m", "Initial commit")
|
git_command(settings.NOTES_STORAGE_DIR, "commit", "-m", "Initial commit")
|
||||||
git_command(settings.NOTES_STORAGE_DIR, "push", "-u", "origin", "main")
|
git_command(settings.NOTES_STORAGE_DIR, "push", "-u", "origin", "main")
|
||||||
return {"status": "success"}
|
return {"status": "success"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(name=TRACK_GIT_CHANGES)
|
||||||
|
@safe_task_execution
|
||||||
|
def track_git_changes():
|
||||||
|
"""Track git changes by noting current commit, pulling new commits, and listing changed files."""
|
||||||
|
logger.info("Tracking git changes")
|
||||||
|
|
||||||
|
repo_root = settings.NOTES_STORAGE_DIR
|
||||||
|
if not (repo_root / ".git").exists():
|
||||||
|
logger.warning("Git repository not found")
|
||||||
|
return {"status": "no_git_repo"}
|
||||||
|
|
||||||
|
current_branch = check_git_command(repo_root, "rev-parse", "--abbrev-ref", "HEAD")
|
||||||
|
current_commit = check_git_command(repo_root, "rev-parse", "HEAD")
|
||||||
|
check_git_command(repo_root, "fetch", "origin")
|
||||||
|
git_command(repo_root, "pull", "origin", current_branch)
|
||||||
|
latest_commit = check_git_command(
|
||||||
|
repo_root, "rev-parse", f"origin/{current_branch}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if there are any changes
|
||||||
|
if current_commit == latest_commit:
|
||||||
|
logger.info("No new changes")
|
||||||
|
return {
|
||||||
|
"status": "no_changes",
|
||||||
|
"current_commit": current_commit,
|
||||||
|
"latest_commit": latest_commit,
|
||||||
|
"changed_files": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get list of changed files between current and latest commit
|
||||||
|
diff_result = git_command(
|
||||||
|
repo_root, "diff", "--name-only", f"{current_commit}..{latest_commit}"
|
||||||
|
)
|
||||||
|
if diff_result and diff_result.returncode == 0:
|
||||||
|
changed_files = [
|
||||||
|
f.strip() for f in diff_result.stdout.strip().split("\n") if f.strip()
|
||||||
|
]
|
||||||
|
logger.info(f"Changed files: {changed_files}")
|
||||||
|
else:
|
||||||
|
logger.error("Failed to get changed files")
|
||||||
|
return {"status": "error", "error": "Failed to get changed files"}
|
||||||
|
|
||||||
|
for file in changed_files:
|
||||||
|
file = pathlib.Path(file)
|
||||||
|
sync_note.delay(
|
||||||
|
subject=file.stem,
|
||||||
|
content=file.read_text(),
|
||||||
|
filename=file.as_posix(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"current_commit": current_commit,
|
||||||
|
"latest_commit": latest_commit,
|
||||||
|
"changed_files": changed_files,
|
||||||
|
}
|
||||||
|
@ -475,3 +475,573 @@ def test_sync_notes_handles_file_read_errors(mock_sync_note, db_session):
|
|||||||
# Should catch the error and return error status
|
# Should catch the error and return error status
|
||||||
assert result["status"] == "error"
|
assert result["status"] == "error"
|
||||||
assert "File read error" in result["error"]
|
assert "File read error" in result["error"]
|
||||||
|
|
||||||
|
|
||||||
|
@patch("memory.workers.tasks.notes.git_command")
|
||||||
|
def test_check_git_command_success(mock_git_command):
|
||||||
|
"""Test check_git_command with successful git command execution."""
|
||||||
|
# Mock successful git command
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.returncode = 0
|
||||||
|
mock_result.stdout = " main \n" # Test that it strips whitespace
|
||||||
|
mock_result.stderr = ""
|
||||||
|
mock_git_command.return_value = mock_result
|
||||||
|
|
||||||
|
repo_root = pathlib.Path("/test/repo")
|
||||||
|
result = notes.check_git_command(repo_root, "branch", "--show-current")
|
||||||
|
|
||||||
|
assert result == "main"
|
||||||
|
mock_git_command.assert_called_once_with(
|
||||||
|
repo_root, "branch", "--show-current", force=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("memory.workers.tasks.notes.git_command")
|
||||||
|
def test_check_git_command_with_force(mock_git_command):
|
||||||
|
"""Test check_git_command with force=True parameter."""
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.returncode = 0
|
||||||
|
mock_result.stdout = "output"
|
||||||
|
mock_result.stderr = ""
|
||||||
|
mock_git_command.return_value = mock_result
|
||||||
|
|
||||||
|
repo_root = pathlib.Path("/test/repo")
|
||||||
|
result = notes.check_git_command(repo_root, "status", force=True)
|
||||||
|
|
||||||
|
assert result == "output"
|
||||||
|
mock_git_command.assert_called_once_with(repo_root, "status", force=True)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("memory.workers.tasks.notes.git_command")
|
||||||
|
def test_check_git_command_no_git_repo(mock_git_command):
|
||||||
|
"""Test check_git_command when git_command returns None (no git repo)."""
|
||||||
|
mock_git_command.return_value = None
|
||||||
|
|
||||||
|
repo_root = pathlib.Path("/test/repo")
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match=r"`status` failed"):
|
||||||
|
notes.check_git_command(repo_root, "status")
|
||||||
|
|
||||||
|
mock_git_command.assert_called_once_with(repo_root, "status", force=False)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("memory.workers.tasks.notes.git_command")
|
||||||
|
def test_check_git_command_git_failure(mock_git_command):
|
||||||
|
"""Test check_git_command when git command fails with non-zero return code."""
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.returncode = 1
|
||||||
|
mock_result.stdout = "fatal: not a git repository"
|
||||||
|
mock_result.stderr = "error: unknown command"
|
||||||
|
mock_git_command.return_value = mock_result
|
||||||
|
|
||||||
|
repo_root = pathlib.Path("/test/repo")
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
RuntimeError, match=r"`branch --invalid` failed with return code 1"
|
||||||
|
):
|
||||||
|
notes.check_git_command(repo_root, "branch", "--invalid")
|
||||||
|
|
||||||
|
mock_git_command.assert_called_once_with(
|
||||||
|
repo_root, "branch", "--invalid", force=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("memory.workers.tasks.notes.git_command")
|
||||||
|
def test_check_git_command_multiple_args(mock_git_command):
|
||||||
|
"""Test check_git_command with multiple arguments."""
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.returncode = 0
|
||||||
|
mock_result.stdout = "commit-hash"
|
||||||
|
mock_result.stderr = ""
|
||||||
|
mock_git_command.return_value = mock_result
|
||||||
|
|
||||||
|
repo_root = pathlib.Path("/test/repo")
|
||||||
|
result = notes.check_git_command(repo_root, "rev-parse", "--short", "HEAD")
|
||||||
|
|
||||||
|
assert result == "commit-hash"
|
||||||
|
mock_git_command.assert_called_once_with(
|
||||||
|
repo_root, "rev-parse", "--short", "HEAD", force=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("memory.workers.tasks.notes.git_command")
|
||||||
|
def test_check_git_command_empty_stdout(mock_git_command):
|
||||||
|
"""Test check_git_command when git command succeeds but returns empty stdout."""
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.returncode = 0
|
||||||
|
mock_result.stdout = ""
|
||||||
|
mock_result.stderr = ""
|
||||||
|
mock_git_command.return_value = mock_result
|
||||||
|
|
||||||
|
repo_root = pathlib.Path("/test/repo")
|
||||||
|
result = notes.check_git_command(repo_root, "diff", "--exit-code")
|
||||||
|
|
||||||
|
assert result == ""
|
||||||
|
mock_git_command.assert_called_once_with(
|
||||||
|
repo_root, "diff", "--exit-code", force=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("memory.workers.tasks.notes.git_command")
|
||||||
|
def test_check_git_command_whitespace_handling(mock_git_command):
|
||||||
|
"""Test check_git_command properly strips whitespace from stdout."""
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.returncode = 0
|
||||||
|
mock_result.stdout = "\n\n some output with spaces \n\n"
|
||||||
|
mock_result.stderr = ""
|
||||||
|
mock_git_command.return_value = mock_result
|
||||||
|
|
||||||
|
repo_root = pathlib.Path("/test/repo")
|
||||||
|
result = notes.check_git_command(repo_root, "log", "--oneline", "-1")
|
||||||
|
|
||||||
|
assert result == "some output with spaces"
|
||||||
|
mock_git_command.assert_called_once_with(
|
||||||
|
repo_root, "log", "--oneline", "-1", force=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("memory.workers.tasks.notes.git_command")
|
||||||
|
@patch("memory.workers.tasks.notes.logger")
|
||||||
|
def test_check_git_command_logs_errors(mock_logger, mock_git_command):
|
||||||
|
"""Test check_git_command logs error details when git command fails."""
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.returncode = 128
|
||||||
|
mock_result.stdout = "some output"
|
||||||
|
mock_result.stderr = "fatal: repository not found"
|
||||||
|
mock_git_command.return_value = mock_result
|
||||||
|
|
||||||
|
repo_root = pathlib.Path("/test/repo")
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
notes.check_git_command(repo_root, "clone", "invalid-url")
|
||||||
|
|
||||||
|
# Verify error logging
|
||||||
|
mock_logger.error.assert_any_call("Git command failed: 128")
|
||||||
|
mock_logger.error.assert_any_call("stderr: fatal: repository not found")
|
||||||
|
mock_logger.error.assert_any_call("stdout: some output")
|
||||||
|
|
||||||
|
|
||||||
|
@patch("memory.workers.tasks.notes.git_command")
|
||||||
|
@patch("memory.workers.tasks.notes.logger")
|
||||||
|
def test_check_git_command_logs_errors_no_stdout(mock_logger, mock_git_command):
|
||||||
|
"""Test check_git_command logs appropriately when there's no stdout."""
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.returncode = 1
|
||||||
|
mock_result.stdout = ""
|
||||||
|
mock_result.stderr = "error: command failed"
|
||||||
|
mock_git_command.return_value = mock_result
|
||||||
|
|
||||||
|
repo_root = pathlib.Path("/test/repo")
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
notes.check_git_command(repo_root, "invalid-command")
|
||||||
|
|
||||||
|
# Verify error logging - should not log stdout when empty
|
||||||
|
mock_logger.error.assert_any_call("Git command failed: 1")
|
||||||
|
mock_logger.error.assert_any_call("stderr: error: command failed")
|
||||||
|
# stdout logging should not have been called since stdout is empty
|
||||||
|
stdout_calls = [
|
||||||
|
call for call in mock_logger.error.call_args_list if "stdout:" in str(call)
|
||||||
|
]
|
||||||
|
assert len(stdout_calls) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@patch("memory.workers.tasks.notes.settings")
|
||||||
|
def test_track_git_changes_no_git_repo(mock_settings):
|
||||||
|
"""Test track_git_changes when no git repository exists."""
|
||||||
|
mock_repo_root = Mock()
|
||||||
|
mock_repo_root.__truediv__ = Mock(return_value=Mock())
|
||||||
|
mock_repo_root.__truediv__.return_value.exists.return_value = False
|
||||||
|
mock_settings.NOTES_STORAGE_DIR = mock_repo_root
|
||||||
|
|
||||||
|
result = notes.track_git_changes()
|
||||||
|
|
||||||
|
assert result == {"status": "no_git_repo"}
|
||||||
|
|
||||||
|
|
||||||
|
@patch("memory.workers.tasks.notes.sync_note")
|
||||||
|
@patch("memory.workers.tasks.notes.git_command")
|
||||||
|
@patch("memory.workers.tasks.notes.check_git_command")
|
||||||
|
@patch("memory.workers.tasks.notes.settings")
|
||||||
|
def test_track_git_changes_no_changes(
|
||||||
|
mock_settings, mock_check_git, mock_git_command, mock_sync_note
|
||||||
|
):
|
||||||
|
"""Test track_git_changes when there are no new changes."""
|
||||||
|
# Mock git repo exists
|
||||||
|
mock_repo_root = Mock()
|
||||||
|
mock_repo_root.__truediv__ = Mock(return_value=Mock())
|
||||||
|
mock_repo_root.__truediv__.return_value.exists.return_value = True
|
||||||
|
mock_settings.NOTES_STORAGE_DIR = mock_repo_root
|
||||||
|
|
||||||
|
# Mock git commands to return same commit hash
|
||||||
|
mock_check_git.side_effect = [
|
||||||
|
"main", # current branch
|
||||||
|
"abc123", # current commit
|
||||||
|
None, # fetch origin (no return needed)
|
||||||
|
"abc123", # latest commit (same as current)
|
||||||
|
]
|
||||||
|
mock_git_command.return_value = Mock() # pull command
|
||||||
|
|
||||||
|
result = notes.track_git_changes()
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"status": "no_changes",
|
||||||
|
"current_commit": "abc123",
|
||||||
|
"latest_commit": "abc123",
|
||||||
|
"changed_files": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Should not call sync_note when no changes
|
||||||
|
mock_sync_note.delay.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@patch("memory.workers.tasks.notes.sync_note")
|
||||||
|
@patch("memory.workers.tasks.notes.git_command")
|
||||||
|
@patch("memory.workers.tasks.notes.check_git_command")
|
||||||
|
@patch("memory.workers.tasks.notes.settings")
|
||||||
|
def test_track_git_changes_with_changes_success(
|
||||||
|
mock_settings, mock_check_git, mock_git_command, mock_sync_note
|
||||||
|
):
|
||||||
|
"""Test track_git_changes when there are changes and diff succeeds."""
|
||||||
|
# Mock git repo exists
|
||||||
|
mock_repo_root = Mock()
|
||||||
|
mock_repo_root.__truediv__ = Mock(return_value=Mock())
|
||||||
|
mock_repo_root.__truediv__.return_value.exists.return_value = True
|
||||||
|
mock_settings.NOTES_STORAGE_DIR = mock_repo_root
|
||||||
|
|
||||||
|
# Mock git commands
|
||||||
|
mock_check_git.side_effect = [
|
||||||
|
"main", # current branch
|
||||||
|
"abc123", # current commit
|
||||||
|
None, # fetch origin
|
||||||
|
"def456", # latest commit (different from current)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Mock pull command
|
||||||
|
mock_git_command.side_effect = [
|
||||||
|
Mock(), # pull command
|
||||||
|
Mock(returncode=0, stdout="file1.md\nfile2.md\n"), # diff command
|
||||||
|
]
|
||||||
|
|
||||||
|
# Mock file reading
|
||||||
|
mock_file1 = Mock()
|
||||||
|
mock_file1.stem = "file1"
|
||||||
|
mock_file1.read_text.return_value = "Content of file 1"
|
||||||
|
mock_file1.as_posix.return_value = "file1.md"
|
||||||
|
|
||||||
|
mock_file2 = Mock()
|
||||||
|
mock_file2.stem = "file2"
|
||||||
|
mock_file2.read_text.return_value = "Content of file 2"
|
||||||
|
mock_file2.as_posix.return_value = "file2.md"
|
||||||
|
|
||||||
|
with patch("memory.workers.tasks.notes.pathlib.Path") as mock_path:
|
||||||
|
mock_path.side_effect = [mock_file1, mock_file2]
|
||||||
|
|
||||||
|
result = notes.track_git_changes()
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"status": "success",
|
||||||
|
"current_commit": "abc123",
|
||||||
|
"latest_commit": "def456",
|
||||||
|
"changed_files": ["file1.md", "file2.md"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Should call sync_note for each changed file
|
||||||
|
assert mock_sync_note.delay.call_count == 2
|
||||||
|
mock_sync_note.delay.assert_any_call(
|
||||||
|
subject="file1",
|
||||||
|
content="Content of file 1",
|
||||||
|
filename="file1.md",
|
||||||
|
)
|
||||||
|
mock_sync_note.delay.assert_any_call(
|
||||||
|
subject="file2",
|
||||||
|
content="Content of file 2",
|
||||||
|
filename="file2.md",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("memory.workers.tasks.notes.sync_note")
|
||||||
|
@patch("memory.workers.tasks.notes.git_command")
|
||||||
|
@patch("memory.workers.tasks.notes.check_git_command")
|
||||||
|
@patch("memory.workers.tasks.notes.settings")
|
||||||
|
def test_track_git_changes_diff_failure(
|
||||||
|
mock_settings, mock_check_git, mock_git_command, mock_sync_note
|
||||||
|
):
|
||||||
|
"""Test track_git_changes when diff command fails."""
|
||||||
|
# Mock git repo exists
|
||||||
|
mock_repo_root = Mock()
|
||||||
|
mock_repo_root.__truediv__ = Mock(return_value=Mock())
|
||||||
|
mock_repo_root.__truediv__.return_value.exists.return_value = True
|
||||||
|
mock_settings.NOTES_STORAGE_DIR = mock_repo_root
|
||||||
|
|
||||||
|
# Mock git commands
|
||||||
|
mock_check_git.side_effect = [
|
||||||
|
"main", # current branch
|
||||||
|
"abc123", # current commit
|
||||||
|
None, # fetch origin
|
||||||
|
"def456", # latest commit (different from current)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Mock pull command success, diff command failure
|
||||||
|
mock_git_command.side_effect = [
|
||||||
|
Mock(), # pull command
|
||||||
|
Mock(returncode=1, stdout="", stderr="diff failed"), # diff command fails
|
||||||
|
]
|
||||||
|
|
||||||
|
result = notes.track_git_changes()
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"status": "error",
|
||||||
|
"error": "Failed to get changed files",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Should not call sync_note when diff fails
|
||||||
|
mock_sync_note.delay.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@patch("memory.workers.tasks.notes.sync_note")
|
||||||
|
@patch("memory.workers.tasks.notes.git_command")
|
||||||
|
@patch("memory.workers.tasks.notes.check_git_command")
|
||||||
|
@patch("memory.workers.tasks.notes.settings")
|
||||||
|
def test_track_git_changes_diff_returns_none(
|
||||||
|
mock_settings, mock_check_git, mock_git_command, mock_sync_note
|
||||||
|
):
|
||||||
|
"""Test track_git_changes when diff command returns None."""
|
||||||
|
# Mock git repo exists
|
||||||
|
mock_repo_root = Mock()
|
||||||
|
mock_repo_root.__truediv__ = Mock(return_value=Mock())
|
||||||
|
mock_repo_root.__truediv__.return_value.exists.return_value = True
|
||||||
|
mock_settings.NOTES_STORAGE_DIR = mock_repo_root
|
||||||
|
|
||||||
|
# Mock git commands
|
||||||
|
mock_check_git.side_effect = [
|
||||||
|
"main", # current branch
|
||||||
|
"abc123", # current commit
|
||||||
|
None, # fetch origin
|
||||||
|
"def456", # latest commit (different from current)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Mock pull command success, diff command returns None
|
||||||
|
mock_git_command.side_effect = [
|
||||||
|
Mock(), # pull command
|
||||||
|
None, # diff command returns None
|
||||||
|
]
|
||||||
|
|
||||||
|
result = notes.track_git_changes()
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"status": "error",
|
||||||
|
"error": "Failed to get changed files",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Should not call sync_note when diff returns None
|
||||||
|
mock_sync_note.delay.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@patch("memory.workers.tasks.notes.sync_note")
|
||||||
|
@patch("memory.workers.tasks.notes.git_command")
|
||||||
|
@patch("memory.workers.tasks.notes.check_git_command")
|
||||||
|
@patch("memory.workers.tasks.notes.settings")
|
||||||
|
def test_track_git_changes_empty_diff(
|
||||||
|
mock_settings, mock_check_git, mock_git_command, mock_sync_note
|
||||||
|
):
|
||||||
|
"""Test track_git_changes when diff returns empty (no actual file changes)."""
|
||||||
|
# Mock git repo exists
|
||||||
|
mock_repo_root = Mock()
|
||||||
|
mock_repo_root.__truediv__ = Mock(return_value=Mock())
|
||||||
|
mock_repo_root.__truediv__.return_value.exists.return_value = True
|
||||||
|
mock_settings.NOTES_STORAGE_DIR = mock_repo_root
|
||||||
|
|
||||||
|
# Mock git commands
|
||||||
|
mock_check_git.side_effect = [
|
||||||
|
"main", # current branch
|
||||||
|
"abc123", # current commit
|
||||||
|
None, # fetch origin
|
||||||
|
"def456", # latest commit (different from current)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Mock pull command success, diff command returns empty
|
||||||
|
mock_git_command.side_effect = [
|
||||||
|
Mock(), # pull command
|
||||||
|
Mock(returncode=0, stdout=""), # diff command returns empty
|
||||||
|
]
|
||||||
|
|
||||||
|
result = notes.track_git_changes()
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"status": "success",
|
||||||
|
"current_commit": "abc123",
|
||||||
|
"latest_commit": "def456",
|
||||||
|
"changed_files": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Should not call sync_note when no files changed
|
||||||
|
mock_sync_note.delay.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@patch("memory.workers.tasks.notes.sync_note")
|
||||||
|
@patch("memory.workers.tasks.notes.git_command")
|
||||||
|
@patch("memory.workers.tasks.notes.check_git_command")
|
||||||
|
@patch("memory.workers.tasks.notes.settings")
|
||||||
|
def test_track_git_changes_whitespace_in_filenames(
|
||||||
|
mock_settings, mock_check_git, mock_git_command, mock_sync_note
|
||||||
|
):
|
||||||
|
"""Test track_git_changes handles whitespace in filenames correctly."""
|
||||||
|
# Mock git repo exists
|
||||||
|
mock_repo_root = Mock()
|
||||||
|
mock_repo_root.__truediv__ = Mock(return_value=Mock())
|
||||||
|
mock_repo_root.__truediv__.return_value.exists.return_value = True
|
||||||
|
mock_settings.NOTES_STORAGE_DIR = mock_repo_root
|
||||||
|
|
||||||
|
# Mock git commands
|
||||||
|
mock_check_git.side_effect = [
|
||||||
|
"main", # current branch
|
||||||
|
"abc123", # current commit
|
||||||
|
None, # fetch origin
|
||||||
|
"def456", # latest commit (different from current)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Mock diff with whitespace and empty lines
|
||||||
|
mock_git_command.side_effect = [
|
||||||
|
Mock(), # pull command
|
||||||
|
Mock(
|
||||||
|
returncode=0, stdout=" file1.md \n\n file2.md \n\n"
|
||||||
|
), # diff with whitespace
|
||||||
|
]
|
||||||
|
|
||||||
|
# Mock file reading
|
||||||
|
mock_file1 = Mock()
|
||||||
|
mock_file1.stem = "file1"
|
||||||
|
mock_file1.read_text.return_value = "Content 1"
|
||||||
|
mock_file1.as_posix.return_value = "file1.md"
|
||||||
|
|
||||||
|
mock_file2 = Mock()
|
||||||
|
mock_file2.stem = "file2"
|
||||||
|
mock_file2.read_text.return_value = "Content 2"
|
||||||
|
mock_file2.as_posix.return_value = "file2.md"
|
||||||
|
|
||||||
|
with patch("memory.workers.tasks.notes.pathlib.Path") as mock_path:
|
||||||
|
mock_path.side_effect = [mock_file1, mock_file2]
|
||||||
|
|
||||||
|
result = notes.track_git_changes()
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"status": "success",
|
||||||
|
"current_commit": "abc123",
|
||||||
|
"latest_commit": "def456",
|
||||||
|
"changed_files": ["file1.md", "file2.md"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Should call sync_note for each non-empty file
|
||||||
|
assert mock_sync_note.delay.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
@patch("memory.workers.tasks.notes.sync_note")
|
||||||
|
@patch("memory.workers.tasks.notes.git_command")
|
||||||
|
@patch("memory.workers.tasks.notes.check_git_command")
|
||||||
|
@patch("memory.workers.tasks.notes.settings")
|
||||||
|
def test_track_git_changes_feature_branch(
|
||||||
|
mock_settings, mock_check_git, mock_git_command, mock_sync_note
|
||||||
|
):
|
||||||
|
"""Test track_git_changes works with feature branches."""
|
||||||
|
# Mock git repo exists
|
||||||
|
mock_repo_root = Mock()
|
||||||
|
mock_repo_root.__truediv__ = Mock(return_value=Mock())
|
||||||
|
mock_repo_root.__truediv__.return_value.exists.return_value = True
|
||||||
|
mock_settings.NOTES_STORAGE_DIR = mock_repo_root
|
||||||
|
|
||||||
|
# Mock git commands for feature branch
|
||||||
|
mock_check_git.side_effect = [
|
||||||
|
"feature/notes-sync", # current branch
|
||||||
|
"abc123", # current commit
|
||||||
|
None, # fetch origin
|
||||||
|
"def456", # latest commit from origin/feature/notes-sync
|
||||||
|
]
|
||||||
|
|
||||||
|
mock_git_command.side_effect = [
|
||||||
|
Mock(), # pull origin feature/notes-sync
|
||||||
|
Mock(returncode=0, stdout="feature_file.md\n"), # diff command
|
||||||
|
]
|
||||||
|
|
||||||
|
# Mock file reading
|
||||||
|
mock_file = Mock()
|
||||||
|
mock_file.stem = "feature_file"
|
||||||
|
mock_file.read_text.return_value = "Feature content"
|
||||||
|
mock_file.as_posix.return_value = "feature_file.md"
|
||||||
|
|
||||||
|
with patch("memory.workers.tasks.notes.pathlib.Path") as mock_path:
|
||||||
|
mock_path.return_value = mock_file
|
||||||
|
|
||||||
|
result = notes.track_git_changes()
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"status": "success",
|
||||||
|
"current_commit": "abc123",
|
||||||
|
"latest_commit": "def456",
|
||||||
|
"changed_files": ["feature_file.md"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify correct branch was used in git commands
|
||||||
|
mock_git_command.assert_any_call(
|
||||||
|
mock_repo_root, "pull", "origin", "feature/notes-sync"
|
||||||
|
)
|
||||||
|
mock_check_git.assert_any_call(
|
||||||
|
mock_repo_root, "rev-parse", "origin/feature/notes-sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("memory.workers.tasks.notes.sync_note")
|
||||||
|
@patch("memory.workers.tasks.notes.git_command")
|
||||||
|
@patch("memory.workers.tasks.notes.check_git_command")
|
||||||
|
@patch("memory.workers.tasks.notes.settings")
|
||||||
|
@patch("memory.workers.tasks.notes.logger")
|
||||||
|
def test_track_git_changes_logging(
|
||||||
|
mock_logger, mock_settings, mock_check_git, mock_git_command, mock_sync_note
|
||||||
|
):
|
||||||
|
"""Test track_git_changes logs appropriately."""
|
||||||
|
# Mock git repo exists
|
||||||
|
mock_repo_root = Mock()
|
||||||
|
mock_repo_root.__truediv__ = Mock(return_value=Mock())
|
||||||
|
mock_repo_root.__truediv__.return_value.exists.return_value = True
|
||||||
|
mock_settings.NOTES_STORAGE_DIR = mock_repo_root
|
||||||
|
|
||||||
|
# Test no changes scenario
|
||||||
|
mock_check_git.side_effect = [
|
||||||
|
"main", # current branch
|
||||||
|
"abc123", # current commit
|
||||||
|
None, # fetch origin
|
||||||
|
"abc123", # latest commit (same as current)
|
||||||
|
]
|
||||||
|
mock_git_command.return_value = Mock() # pull command
|
||||||
|
|
||||||
|
notes.track_git_changes()
|
||||||
|
|
||||||
|
# Verify logging
|
||||||
|
mock_logger.info.assert_any_call("Tracking git changes")
|
||||||
|
mock_logger.info.assert_any_call("No new changes")
|
||||||
|
|
||||||
|
# Reset mocks for changes scenario
|
||||||
|
mock_logger.reset_mock()
|
||||||
|
mock_check_git.side_effect = [
|
||||||
|
"main", # current branch
|
||||||
|
"abc123", # current commit
|
||||||
|
None, # fetch origin
|
||||||
|
"def456", # latest commit (different)
|
||||||
|
]
|
||||||
|
mock_git_command.side_effect = [
|
||||||
|
Mock(), # pull command
|
||||||
|
Mock(returncode=0, stdout="test.md\n"), # diff command
|
||||||
|
]
|
||||||
|
|
||||||
|
mock_file = Mock()
|
||||||
|
mock_file.stem = "test"
|
||||||
|
mock_file.read_text.return_value = "Test content"
|
||||||
|
mock_file.as_posix.return_value = "test.md"
|
||||||
|
|
||||||
|
with patch("memory.workers.tasks.notes.pathlib.Path") as mock_path:
|
||||||
|
mock_path.return_value = mock_file
|
||||||
|
notes.track_git_changes()
|
||||||
|
|
||||||
|
# Verify logging for changes scenario
|
||||||
|
mock_logger.info.assert_any_call("Tracking git changes")
|
||||||
|
mock_logger.info.assert_any_call("Changed files: ['test.md']")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user