feat(12-02): add refresh_cloud_folder Celery task, staleness trigger, version 0.1.5
- Add tasks/cloud_tasks.py: durable refresh_cloud_folder task with 3-retry bounded backoff (30s/90s/270s +jitter), credential decryption in worker only - Register tasks.cloud_tasks.* on documents queue in celery_app.py - Add stale-while-revalidate staleness trigger in browse.py (5-min threshold) - Add 4 Task 3 tests: idempotency, cached-row retention on failure, task structure, no-byte-download contract; add background-refresh scheduling integration test - Bump backend version 0.1.4 → 0.1.5, frontend package.json 0.1.4 → 0.1.5 - Update AGENTS.md with Phase 12 Plan 02 state and new shared module map entries - Update README with connection-ID browse API table and v0.1.5
This commit is contained in:
@@ -1081,3 +1081,81 @@ async def test_list_connections_returns_all_providers(async_client, db_session):
|
||||
# credentials_enc must not be in any item
|
||||
for item in items:
|
||||
assert "credentials_enc" not in item
|
||||
|
||||
|
||||
async def test_browse_connection_schedules_background_refresh_on_cached_items(
|
||||
async_client, db_session, monkeypatch
|
||||
):
|
||||
"""GET /api/cloud/connections/{id}/items schedules Celery refresh when cached items exist.
|
||||
|
||||
Phase 12 stale-while-revalidate: if items are already cached and folder_state
|
||||
has last_refreshed_at set, the endpoint returns immediately and schedules a
|
||||
background refresh via refresh_cloud_folder.delay().
|
||||
Verifies: .delay() is called with the correct user_id, connection_id, parent_ref.
|
||||
"""
|
||||
from unittest.mock import patch, MagicMock
|
||||
from db.models import CloudItem, CloudFolderState
|
||||
from storage.cloud_utils import encrypt_credentials
|
||||
|
||||
auth = await _create_user_and_token(db_session, role="user")
|
||||
uid_str = str(auth["user"].id)
|
||||
master_key = b"test-key-for-testing-32bytes!!"
|
||||
creds_enc = encrypt_credentials(master_key, uid_str, {"access_token": "tok"})
|
||||
|
||||
conn = await _create_cloud_connection(
|
||||
db_session, auth["user"].id, provider="google_drive", name="Stale Drive"
|
||||
)
|
||||
conn_id_str = str(conn.id)
|
||||
|
||||
# Seed a durable CloudItem so browse sees existing rows
|
||||
from datetime import datetime, timezone
|
||||
item = CloudItem(
|
||||
id=_uuid.uuid4(),
|
||||
user_id=auth["user"].id,
|
||||
connection_id=conn.id,
|
||||
provider_item_id="cached-item-001",
|
||||
name="cached.pdf",
|
||||
kind="file",
|
||||
analysis_status="pending",
|
||||
semantic_index_status="none",
|
||||
)
|
||||
db_session.add(item)
|
||||
|
||||
# Seed a CloudFolderState with last_refreshed_at set (non-first-visit)
|
||||
from datetime import timedelta
|
||||
fs = CloudFolderState(
|
||||
id=_uuid.uuid4(),
|
||||
user_id=auth["user"].id,
|
||||
connection_id=conn.id,
|
||||
parent_ref="",
|
||||
refresh_state="fresh",
|
||||
last_refreshed_at=datetime.now(timezone.utc) - timedelta(minutes=5),
|
||||
)
|
||||
db_session.add(fs)
|
||||
await db_session.commit()
|
||||
|
||||
delay_mock = MagicMock()
|
||||
with patch("tasks.cloud_tasks.refresh_cloud_folder") as mock_task:
|
||||
mock_task.delay = delay_mock
|
||||
# Patch capability probe to avoid real network call
|
||||
with patch("api.cloud.browse.build_cloud_resource_adapter") as mock_adapter_factory:
|
||||
from storage.cloud_base import CloudCapability, STATE_SUPPORTED, ACTIONS
|
||||
mock_caps = {a: CloudCapability(action=a, state=STATE_SUPPORTED) for a in ACTIONS}
|
||||
mock_adapter = MagicMock()
|
||||
mock_adapter.get_capabilities = _uuid.__class__ # callable stub
|
||||
import asyncio as _asyncio
|
||||
|
||||
async def _fake_caps(*args, **kwargs):
|
||||
return mock_caps
|
||||
|
||||
mock_adapter.get_capabilities = _fake_caps
|
||||
mock_adapter_factory.return_value = mock_adapter
|
||||
|
||||
resp = await async_client.get(
|
||||
f"/api/cloud/connections/{conn_id_str}/items",
|
||||
headers=auth["headers"],
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
# Background refresh was scheduled
|
||||
delay_mock.assert_called_once_with(uid_str, conn_id_str, None)
|
||||
|
||||
Reference in New Issue
Block a user