Skip to content

Commit 7ccec7e

Browse files
jope-bmclaude
andauthored
feat: Add run_in_background parameter to sync endpoint with tests (#417)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 021af74 commit 7ccec7e

2 files changed

Lines changed: 109 additions & 11 deletions

File tree

src/basic_memory/api/routers/project_router.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ async def sync_project(
113113
force_full: bool = Query(
114114
False, description="Force full scan, bypassing watermark optimization"
115115
),
116+
run_in_background: bool = Query(True, description="Run in background")
116117
):
117118
"""Force project filesystem sync to database.
118119
@@ -123,21 +124,29 @@ async def sync_project(
123124
sync_service: Sync service for this project
124125
project_config: Project configuration
125126
force_full: If True, force a full scan even if watermark exists
127+
run_in_background: If True, run sync in background and return immediately
126128
127129
Returns:
128-
Response confirming sync was initiated
130+
Response confirming sync was initiated (background) or SyncReportResponse (foreground)
129131
"""
130-
background_tasks.add_task(
131-
sync_service.sync, project_config.home, project_config.name, force_full=force_full
132-
)
133-
logger.info(
134-
f"Filesystem sync initiated for project: {project_config.name} (force_full={force_full})"
135-
)
132+
if run_in_background:
133+
background_tasks.add_task(
134+
sync_service.sync, project_config.home, project_config.name, force_full=force_full
135+
)
136+
logger.info(
137+
f"Filesystem sync initiated for project: {project_config.name} (force_full={force_full})"
138+
)
136139

137-
return {
138-
"status": "sync_started",
139-
"message": f"Filesystem sync initiated for project '{project_config.name}'",
140-
}
140+
return {
141+
"status": "sync_started",
142+
"message": f"Filesystem sync initiated for project '{project_config.name}'",
143+
}
144+
else:
145+
report = await sync_service.sync(project_config.home, project_config.name, force_full=force_full)
146+
logger.info(
147+
f"Filesystem sync completed for project: {project_config.name} (force_full={force_full})"
148+
)
149+
return SyncReportResponse.from_sync_report(report)
141150

142151

143152
@project_router.post("/status", response_model=SyncReportResponse)

tests/api/test_project_router.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,95 @@ async def test_sync_project_endpoint_not_found(client):
510510
assert response.status_code == 404
511511

512512

513+
@pytest.mark.asyncio
514+
async def test_sync_project_endpoint_foreground(test_graph, client, project_url):
515+
"""Test the project sync endpoint with run_in_background=false returns sync report."""
516+
# Call the sync endpoint with run_in_background=false
517+
response = await client.post(f"{project_url}/project/sync?run_in_background=false")
518+
519+
# Verify response
520+
assert response.status_code == 200
521+
data = response.json()
522+
523+
# Check that we get a sync report instead of status message
524+
assert "new" in data
525+
assert "modified" in data
526+
assert "deleted" in data
527+
assert "moves" in data
528+
assert "checksums" in data
529+
assert "skipped_files" in data
530+
assert "total" in data
531+
532+
# Verify these are the right types
533+
assert isinstance(data["new"], list)
534+
assert isinstance(data["modified"], list)
535+
assert isinstance(data["deleted"], list)
536+
assert isinstance(data["moves"], dict)
537+
assert isinstance(data["checksums"], dict)
538+
assert isinstance(data["skipped_files"], list)
539+
assert isinstance(data["total"], int)
540+
541+
542+
@pytest.mark.asyncio
543+
async def test_sync_project_endpoint_foreground_with_force_full(test_graph, client, project_url):
544+
"""Test the project sync endpoint with run_in_background=false and force_full=true."""
545+
# Call the sync endpoint with both parameters
546+
response = await client.post(
547+
f"{project_url}/project/sync?run_in_background=false&force_full=true"
548+
)
549+
550+
# Verify response
551+
assert response.status_code == 200
552+
data = response.json()
553+
554+
# Check that we get a sync report with all expected fields
555+
assert "new" in data
556+
assert "modified" in data
557+
assert "deleted" in data
558+
assert "moves" in data
559+
assert "checksums" in data
560+
assert "skipped_files" in data
561+
assert "total" in data
562+
563+
564+
@pytest.mark.asyncio
565+
async def test_sync_project_endpoint_foreground_with_changes(
566+
test_graph, client, project_config, project_url, tmpdir
567+
):
568+
"""Test foreground sync detects actual file changes."""
569+
# Create a new file in the project directory
570+
import os
571+
from pathlib import Path
572+
573+
test_file = Path(project_config.home) / "new_test_file.md"
574+
test_file.write_text("# New Test File\n\nThis is a test file for sync detection.")
575+
576+
try:
577+
# Call the sync endpoint with run_in_background=false
578+
response = await client.post(f"{project_url}/project/sync?run_in_background=false")
579+
580+
# Verify response
581+
assert response.status_code == 200
582+
data = response.json()
583+
584+
# The sync report should show changes (the new file we created)
585+
assert data["total"] >= 0 # Should have at least detected changes
586+
assert "new" in data
587+
assert "modified" in data
588+
assert "deleted" in data
589+
590+
# At least one of these should have changes
591+
has_changes = (
592+
len(data["new"]) > 0 or len(data["modified"]) > 0 or len(data["deleted"]) > 0
593+
)
594+
assert has_changes or data["total"] >= 0 # Either changes detected or empty sync is valid
595+
596+
finally:
597+
# Clean up the test file
598+
if test_file.exists():
599+
os.remove(test_file)
600+
601+
513602
@pytest.mark.asyncio
514603
async def test_remove_default_project_fails(test_config, client, project_service):
515604
"""Test that removing the default project returns an error."""

0 commit comments

Comments
 (0)