test: 补充知识处理、检索与 Agent 回归用例

覆盖切块、向量检索、工具调用和历史预算;模型相关用例使用桩。
This commit is contained in:
WangShen007 committed 2026-10-04 20:55:00 +08:00
1 parent 2dd062787d
commit d9fe727c3f
123 files changed
+29005

No files matched your search

+219
View File
@@ -0,0 +1,219 @@
"""
Copyright 2024-2026 ReplyPier
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from datetime import datetime, timedelta, timezone
import pytest
from app.models.crm import CrmConnectionStatus, CrmSyncJobStatus
from app.models.lead_capture import LeadCaptureResponse
from app.repositories.crm import (
CrmConnectionRepository, CrmSyncJobRepository, MAX_ATTEMPTS,
)
@pytest.fixture
def conn_repo(db):
return CrmConnectionRepository(db)
@pytest.fixture
def job_repo(db):
return CrmSyncJobRepository(db)
@pytest.fixture
def test_lead_response(db, test_organization, test_agent, test_customer) -> LeadCaptureResponse:
response = LeadCaptureResponse(
organization_id=test_organization.id,
agent_id=test_agent.id,
customer_id=test_customer.id,
field_values={"email": "lead@example.com", "name": "Lead Person"},
consent=True,
qualified=True,
)
db.add(response)
db.commit()
db.refresh(response)
return response
CREDS = {"access_token": "at-1", "refresh_token": "rt-1", "api_domain": None}
def _aware(dt: datetime) -> datetime:
"""SQLite loses tzinfo on refresh; normalize for comparisons (PG keeps it)."""
return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
def _connect(conn_repo, org_id, provider="hubspot", external_id="hub-123", **kwargs):
return conn_repo.create_or_update(
organization_id=org_id,
provider=provider,
external_account_id=external_id,
credentials=CREDS,
display_name="Acme Portal",
**kwargs,
)
class TestCrmConnectionRepository:
def test_credentials_round_trip_encrypted(self, conn_repo, test_organization):
conn = _connect(conn_repo, test_organization.id)
# Stored blob is ciphertext, decrypts back to the original dict.
assert "at-1" not in conn.encrypted_credentials
assert conn_repo.get_credentials(conn) == CREDS
def test_create_or_update_is_an_upsert(self, conn_repo, test_organization):
first = _connect(conn_repo, test_organization.id)
conn_repo.set_status(first, CrmConnectionStatus.EXPIRED.value, last_error="dead token")
reconnected = _connect(conn_repo, test_organization.id, external_id="hub-456")
assert reconnected.id == first.id # same row, not a duplicate
assert reconnected.external_account_id == "hub-456"
assert reconnected.status == CrmConnectionStatus.ACTIVE.value
assert reconnected.last_error is None
def test_get_active_ignores_expired(self, conn_repo, test_organization):
conn = _connect(conn_repo, test_organization.id)
assert conn_repo.get_active(test_organization.id, "hubspot") is not None
conn_repo.set_status(conn, CrmConnectionStatus.EXPIRED.value)
assert conn_repo.get_active(test_organization.id, "hubspot") is None
def test_save_credentials_mirrors_expiries(self, conn_repo, test_organization):
conn = _connect(conn_repo, test_organization.id, provider="pipedrive",
external_id="pd-1")
access_exp = datetime.now(timezone.utc) + timedelta(hours=1)
refresh_exp = datetime.now(timezone.utc) + timedelta(days=60)
conn = conn_repo.save_credentials(
conn, {"access_token": "at-2", "refresh_token": "rt-2"},
access_token_expires_at=access_exp,
refresh_token_expires_at=refresh_exp,
)
assert conn_repo.get_credentials(conn)["access_token"] == "at-2"
assert conn.access_token_expires_at is not None
assert conn.refresh_token_expires_at is not None
assert conn.last_refreshed_at is not None
def test_list_needing_refresh(self, conn_repo, db, test_organization):
conn = _connect(conn_repo, test_organization.id, provider="pipedrive",
external_id="pd-2")
cutoff = datetime.now(timezone.utc) - timedelta(hours=24)
# Freshly connected → not due.
assert conn_repo.list_needing_refresh("pipedrive", cutoff) == []
conn.last_refreshed_at = datetime.now(timezone.utc) - timedelta(days=2)
db.commit()
assert [c.id for c in conn_repo.list_needing_refresh("pipedrive", cutoff)] == [conn.id]
# Expired connections are never swept.
conn_repo.set_status(conn, CrmConnectionStatus.EXPIRED.value)
assert conn_repo.list_needing_refresh("pipedrive", cutoff) == []
class TestCrmSyncJobRepository:
def test_enqueue_is_idempotent(self, job_repo, test_organization, test_lead_response):
job = job_repo.enqueue(test_organization.id, test_lead_response.id, "hubspot")
assert job is not None
assert job.status == CrmSyncJobStatus.PENDING.value
# Same lead + provider → no second job.
assert job_repo.enqueue(test_organization.id, test_lead_response.id, "hubspot") is None
# Same lead, other provider → its own job.
other = job_repo.enqueue(test_organization.id, test_lead_response.id, "pipedrive")
assert other is not None and other.id != job.id
def test_claim_marks_processing_and_respects_due_time(
self, job_repo, db, test_organization, test_lead_response):
due = job_repo.enqueue(test_organization.id, test_lead_response.id, "hubspot")
future = job_repo.enqueue(test_organization.id, test_lead_response.id, "pipedrive")
future.next_attempt_at = datetime.now(timezone.utc) + timedelta(hours=1)
db.commit()
claimed = job_repo.claim_batch(limit=10)
assert [j.id for j in claimed] == [due.id]
assert claimed[0].status == CrmSyncJobStatus.PROCESSING.value
assert claimed[0].started_at is not None
# Already-claimed jobs are not claimed again.
assert job_repo.claim_batch(limit=10) == []
def test_reclaim_stale_returns_crashed_claims(
self, job_repo, db, test_organization, test_lead_response):
job = job_repo.enqueue(test_organization.id, test_lead_response.id, "hubspot")
job_repo.claim_batch(limit=1)
db.refresh(job)
job.started_at = datetime.now(timezone.utc) - timedelta(minutes=30)
db.commit()
assert job_repo.reclaim_stale(older_than_minutes=10) == 1
db.refresh(job)
assert job.status == CrmSyncJobStatus.PENDING.value
assert job.started_at is None
def test_complete_stamps_result_and_connection(
self, job_repo, conn_repo, test_organization, test_lead_response):
conn = _connect(conn_repo, test_organization.id)
job = job_repo.enqueue(test_organization.id, test_lead_response.id, "hubspot")
job = job_repo.complete(job, {"action": "created", "contact_id": "42"},
connection_id=conn.id)
assert job.status == CrmSyncJobStatus.COMPLETED.value
assert job.result["contact_id"] == "42"
assert job.connection_id == conn.id
def test_schedule_retry_backs_off_then_fails_terminally(
self, job_repo, test_organization, test_lead_response):
job = job_repo.enqueue(test_organization.id, test_lead_response.id, "hubspot")
before = datetime.now(timezone.utc)
job = job_repo.schedule_retry(job, "HTTP 502")
assert job.status == CrmSyncJobStatus.PENDING.value
assert job.attempts == 1
assert _aware(job.next_attempt_at) > before
job.attempts = MAX_ATTEMPTS - 1
job = job_repo.schedule_retry(job, "HTTP 502 again")
assert job.status == CrmSyncJobStatus.FAILED.value
assert job.attempts == MAX_ATTEMPTS
def test_retry_after_wins_over_backoff_when_longer(
self, job_repo, test_organization, test_lead_response):
job = job_repo.enqueue(test_organization.id, test_lead_response.id, "hubspot")
job = job_repo.schedule_retry(job, "HTTP 429", retry_after_seconds=7200)
# 7200 > capped backoff (3600) → provider's Retry-After is honored.
assert _aware(job.next_attempt_at) >= datetime.now(timezone.utc) + timedelta(seconds=7000)
def test_skip_pending_for_connection(
self, job_repo, test_organization, test_lead_response):
job = job_repo.enqueue(test_organization.id, test_lead_response.id, "hubspot")
count = job_repo.skip_pending_for_connection(
test_organization.id, "hubspot", "app uninstalled")
assert count == 1
assert job_repo.claim_batch(limit=10) == []
def test_count_recent_failures(self, job_repo, test_organization, test_lead_response):
job = job_repo.enqueue(test_organization.id, test_lead_response.id, "hubspot")
job_repo.fail(job, "boom")
assert job_repo.count_recent_failures(test_organization.id, "hubspot") == 1
assert job_repo.count_recent_failures(test_organization.id, "pipedrive") == 0
def test_reopen_failed_requeues(self, job_repo, test_organization, test_lead_response):
job = job_repo.enqueue(test_organization.id, test_lead_response.id, "hubspot")
job.attempts = 7
job_repo.fail(job, "no_connection")
assert job_repo.reopen_failed(test_organization.id, "hubspot") == 1
job_repo.db.refresh(job)
assert job.status == CrmSyncJobStatus.PENDING.value
assert job.attempts == 0
# Other providers untouched.
assert job_repo.reopen_failed(test_organization.id, "pipedrive") == 0