Spaces:
Sleeping
Sleeping
worker test
Browse files- tests/test_worker_pool.py +327 -0
tests/test_worker_pool.py
CHANGED
|
@@ -1411,6 +1411,19 @@ class TestUserModel(IntegrationBase):
|
|
| 1411 |
credits = Column(Integer, default=100)
|
| 1412 |
|
| 1413 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1414 |
@pytest.fixture
|
| 1415 |
async def integration_db():
|
| 1416 |
"""Create a real SQLite database for integration tests."""
|
|
@@ -1962,6 +1975,320 @@ class TestIntegrationOrphanedJobs:
|
|
| 1962 |
assert "shutdown" in job.error_message.lower()
|
| 1963 |
|
| 1964 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1965 |
if __name__ == "__main__":
|
| 1966 |
pytest.main([__file__, "-v"])
|
| 1967 |
|
|
|
|
|
|
| 1411 |
credits = Column(Integer, default=100)
|
| 1412 |
|
| 1413 |
|
| 1414 |
+
class TestApiKeyUsageModel(IntegrationBase):
|
| 1415 |
+
"""Real API key usage model for integration tests."""
|
| 1416 |
+
__tablename__ = "test_api_key_usage"
|
| 1417 |
+
|
| 1418 |
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 1419 |
+
key_index = Column(Integer, unique=True, index=True, nullable=False)
|
| 1420 |
+
total_requests = Column(Integer, default=0)
|
| 1421 |
+
success_count = Column(Integer, default=0)
|
| 1422 |
+
failure_count = Column(Integer, default=0)
|
| 1423 |
+
last_error = Column(Text, nullable=True)
|
| 1424 |
+
last_used_at = Column(DateTime, nullable=True)
|
| 1425 |
+
|
| 1426 |
+
|
| 1427 |
@pytest.fixture
|
| 1428 |
async def integration_db():
|
| 1429 |
"""Create a real SQLite database for integration tests."""
|
|
|
|
| 1975 |
assert "shutdown" in job.error_message.lower()
|
| 1976 |
|
| 1977 |
|
| 1978 |
+
# =============================================================================
|
| 1979 |
+
# 16. API Key Manager Integration Tests (REAL, not mocked!)
|
| 1980 |
+
# =============================================================================
|
| 1981 |
+
|
| 1982 |
+
class TestIntegrationApiKeyManager:
|
| 1983 |
+
"""
|
| 1984 |
+
Integration tests for api_key_manager.py with REAL database.
|
| 1985 |
+
These tests verify the double-commit fix actually works.
|
| 1986 |
+
"""
|
| 1987 |
+
|
| 1988 |
+
@pytest.mark.asyncio
|
| 1989 |
+
async def test_record_usage_does_not_commit_on_its_own(self, integration_db):
|
| 1990 |
+
"""
|
| 1991 |
+
Verify record_usage does NOT commit - caller must commit.
|
| 1992 |
+
This tests the double-commit fix.
|
| 1993 |
+
"""
|
| 1994 |
+
session_maker = integration_db["session_maker"]
|
| 1995 |
+
|
| 1996 |
+
# Create initial usage record
|
| 1997 |
+
async with session_maker() as session:
|
| 1998 |
+
usage = TestApiKeyUsageModel(
|
| 1999 |
+
key_index=0,
|
| 2000 |
+
total_requests=0,
|
| 2001 |
+
success_count=0,
|
| 2002 |
+
failure_count=0
|
| 2003 |
+
)
|
| 2004 |
+
session.add(usage)
|
| 2005 |
+
await session.commit()
|
| 2006 |
+
|
| 2007 |
+
# Now simulate what record_usage does (without commit)
|
| 2008 |
+
async with session_maker() as session:
|
| 2009 |
+
result = await session.execute(
|
| 2010 |
+
select(TestApiKeyUsageModel).where(TestApiKeyUsageModel.key_index == 0)
|
| 2011 |
+
)
|
| 2012 |
+
usage = result.scalar_one()
|
| 2013 |
+
|
| 2014 |
+
# Modify like record_usage does
|
| 2015 |
+
usage.total_requests += 1
|
| 2016 |
+
usage.success_count += 1
|
| 2017 |
+
usage.last_used_at = datetime.utcnow()
|
| 2018 |
+
|
| 2019 |
+
# DON'T commit - simulating the fixed behavior
|
| 2020 |
+
# await session.commit() # This is what we removed!
|
| 2021 |
+
|
| 2022 |
+
# Session closes without commit - changes should be LOST
|
| 2023 |
+
|
| 2024 |
+
# Verify changes were NOT persisted (rolled back)
|
| 2025 |
+
async with session_maker() as session:
|
| 2026 |
+
result = await session.execute(
|
| 2027 |
+
select(TestApiKeyUsageModel).where(TestApiKeyUsageModel.key_index == 0)
|
| 2028 |
+
)
|
| 2029 |
+
usage = result.scalar_one()
|
| 2030 |
+
|
| 2031 |
+
assert usage.total_requests == 0, "Changes should NOT persist without commit!"
|
| 2032 |
+
|
| 2033 |
+
@pytest.mark.asyncio
|
| 2034 |
+
async def test_usage_persists_when_caller_commits(self, integration_db):
|
| 2035 |
+
"""
|
| 2036 |
+
Verify changes persist when caller commits the transaction.
|
| 2037 |
+
"""
|
| 2038 |
+
session_maker = integration_db["session_maker"]
|
| 2039 |
+
|
| 2040 |
+
# Create initial usage record
|
| 2041 |
+
async with session_maker() as session:
|
| 2042 |
+
usage = TestApiKeyUsageModel(
|
| 2043 |
+
key_index=1,
|
| 2044 |
+
total_requests=0,
|
| 2045 |
+
success_count=0,
|
| 2046 |
+
failure_count=0
|
| 2047 |
+
)
|
| 2048 |
+
session.add(usage)
|
| 2049 |
+
await session.commit()
|
| 2050 |
+
|
| 2051 |
+
# Modify and commit (like _process_job does)
|
| 2052 |
+
async with session_maker() as session:
|
| 2053 |
+
result = await session.execute(
|
| 2054 |
+
select(TestApiKeyUsageModel).where(TestApiKeyUsageModel.key_index == 1)
|
| 2055 |
+
)
|
| 2056 |
+
usage = result.scalar_one()
|
| 2057 |
+
|
| 2058 |
+
usage.total_requests += 1
|
| 2059 |
+
usage.success_count += 1
|
| 2060 |
+
usage.last_used_at = datetime.utcnow()
|
| 2061 |
+
|
| 2062 |
+
# Caller commits (this is correct behavior)
|
| 2063 |
+
await session.commit()
|
| 2064 |
+
|
| 2065 |
+
# Verify changes WERE persisted
|
| 2066 |
+
async with session_maker() as session:
|
| 2067 |
+
result = await session.execute(
|
| 2068 |
+
select(TestApiKeyUsageModel).where(TestApiKeyUsageModel.key_index == 1)
|
| 2069 |
+
)
|
| 2070 |
+
usage = result.scalar_one()
|
| 2071 |
+
|
| 2072 |
+
assert usage.total_requests == 1, "Changes should persist when caller commits"
|
| 2073 |
+
assert usage.success_count == 1
|
| 2074 |
+
|
| 2075 |
+
@pytest.mark.asyncio
|
| 2076 |
+
async def test_least_used_key_selection(self, integration_db):
|
| 2077 |
+
"""
|
| 2078 |
+
Test that the least-used key is selected correctly.
|
| 2079 |
+
"""
|
| 2080 |
+
session_maker = integration_db["session_maker"]
|
| 2081 |
+
|
| 2082 |
+
# Create usage records with different counts
|
| 2083 |
+
async with session_maker() as session:
|
| 2084 |
+
# Key 0: 10 requests
|
| 2085 |
+
usage0 = TestApiKeyUsageModel(key_index=0, total_requests=10)
|
| 2086 |
+
# Key 1: 5 requests (least used)
|
| 2087 |
+
usage1 = TestApiKeyUsageModel(key_index=1, total_requests=5)
|
| 2088 |
+
# Key 2: 15 requests
|
| 2089 |
+
usage2 = TestApiKeyUsageModel(key_index=2, total_requests=15)
|
| 2090 |
+
|
| 2091 |
+
session.add_all([usage0, usage1, usage2])
|
| 2092 |
+
await session.commit()
|
| 2093 |
+
|
| 2094 |
+
# Query for least used (like get_least_used_key does)
|
| 2095 |
+
async with session_maker() as session:
|
| 2096 |
+
result = await session.execute(
|
| 2097 |
+
select(TestApiKeyUsageModel).order_by(TestApiKeyUsageModel.total_requests)
|
| 2098 |
+
)
|
| 2099 |
+
usages = result.scalars().all()
|
| 2100 |
+
|
| 2101 |
+
assert usages[0].key_index == 1, "Key with least requests should be first"
|
| 2102 |
+
assert usages[0].total_requests == 5
|
| 2103 |
+
|
| 2104 |
+
@pytest.mark.asyncio
|
| 2105 |
+
async def test_new_key_created_in_same_transaction(self, integration_db):
|
| 2106 |
+
"""
|
| 2107 |
+
Test that new key creation happens in same transaction (not committed separately).
|
| 2108 |
+
"""
|
| 2109 |
+
session_maker = integration_db["session_maker"]
|
| 2110 |
+
|
| 2111 |
+
# Start transaction, add new key, but DON'T commit
|
| 2112 |
+
async with session_maker() as session:
|
| 2113 |
+
new_usage = TestApiKeyUsageModel(
|
| 2114 |
+
key_index=99,
|
| 2115 |
+
total_requests=0,
|
| 2116 |
+
success_count=0,
|
| 2117 |
+
failure_count=0
|
| 2118 |
+
)
|
| 2119 |
+
session.add(new_usage)
|
| 2120 |
+
# No commit - transaction rolls back
|
| 2121 |
+
|
| 2122 |
+
# Verify key was NOT created
|
| 2123 |
+
async with session_maker() as session:
|
| 2124 |
+
result = await session.execute(
|
| 2125 |
+
select(TestApiKeyUsageModel).where(TestApiKeyUsageModel.key_index == 99)
|
| 2126 |
+
)
|
| 2127 |
+
usage = result.scalar_one_or_none()
|
| 2128 |
+
|
| 2129 |
+
assert usage is None, "Key should not exist without commit"
|
| 2130 |
+
|
| 2131 |
+
@pytest.mark.asyncio
|
| 2132 |
+
async def test_failure_recording(self, integration_db):
|
| 2133 |
+
"""
|
| 2134 |
+
Test that failure count and error message are recorded correctly.
|
| 2135 |
+
"""
|
| 2136 |
+
session_maker = integration_db["session_maker"]
|
| 2137 |
+
|
| 2138 |
+
# Create and modify in same transaction
|
| 2139 |
+
async with session_maker() as session:
|
| 2140 |
+
usage = TestApiKeyUsageModel(
|
| 2141 |
+
key_index=10,
|
| 2142 |
+
total_requests=0,
|
| 2143 |
+
success_count=0,
|
| 2144 |
+
failure_count=0
|
| 2145 |
+
)
|
| 2146 |
+
session.add(usage)
|
| 2147 |
+
|
| 2148 |
+
# Simulate failure
|
| 2149 |
+
usage.total_requests += 1
|
| 2150 |
+
usage.failure_count += 1
|
| 2151 |
+
usage.last_error = "API rate limit exceeded"[:1000]
|
| 2152 |
+
usage.last_used_at = datetime.utcnow()
|
| 2153 |
+
|
| 2154 |
+
await session.commit()
|
| 2155 |
+
|
| 2156 |
+
# Verify
|
| 2157 |
+
async with session_maker() as session:
|
| 2158 |
+
result = await session.execute(
|
| 2159 |
+
select(TestApiKeyUsageModel).where(TestApiKeyUsageModel.key_index == 10)
|
| 2160 |
+
)
|
| 2161 |
+
usage = result.scalar_one()
|
| 2162 |
+
|
| 2163 |
+
assert usage.total_requests == 1
|
| 2164 |
+
assert usage.failure_count == 1
|
| 2165 |
+
assert usage.success_count == 0
|
| 2166 |
+
assert "rate limit" in usage.last_error.lower()
|
| 2167 |
+
|
| 2168 |
+
@pytest.mark.asyncio
|
| 2169 |
+
async def test_transaction_atomicity_job_and_usage(self, integration_db):
|
| 2170 |
+
"""
|
| 2171 |
+
Test that job update and usage recording are atomic.
|
| 2172 |
+
If we fail before commit, NEITHER should persist.
|
| 2173 |
+
"""
|
| 2174 |
+
session_maker = integration_db["session_maker"]
|
| 2175 |
+
|
| 2176 |
+
# Create job and usage
|
| 2177 |
+
async with session_maker() as session:
|
| 2178 |
+
job = TestJobModel(
|
| 2179 |
+
job_id="atomic-job",
|
| 2180 |
+
user_id="user",
|
| 2181 |
+
job_type="text",
|
| 2182 |
+
status="queued",
|
| 2183 |
+
priority="fast",
|
| 2184 |
+
created_at=datetime.utcnow()
|
| 2185 |
+
)
|
| 2186 |
+
usage = TestApiKeyUsageModel(
|
| 2187 |
+
key_index=20,
|
| 2188 |
+
total_requests=0,
|
| 2189 |
+
success_count=0,
|
| 2190 |
+
failure_count=0
|
| 2191 |
+
)
|
| 2192 |
+
session.add_all([job, usage])
|
| 2193 |
+
await session.commit()
|
| 2194 |
+
|
| 2195 |
+
# Simulate processing: update both, but crash before commit
|
| 2196 |
+
async with session_maker() as session:
|
| 2197 |
+
# Update job
|
| 2198 |
+
result = await session.execute(
|
| 2199 |
+
select(TestJobModel).where(TestJobModel.job_id == "atomic-job")
|
| 2200 |
+
)
|
| 2201 |
+
job = result.scalar_one()
|
| 2202 |
+
job.status = "completed"
|
| 2203 |
+
job.completed_at = datetime.utcnow()
|
| 2204 |
+
|
| 2205 |
+
# Update usage
|
| 2206 |
+
result = await session.execute(
|
| 2207 |
+
select(TestApiKeyUsageModel).where(TestApiKeyUsageModel.key_index == 20)
|
| 2208 |
+
)
|
| 2209 |
+
usage = result.scalar_one()
|
| 2210 |
+
usage.total_requests += 1
|
| 2211 |
+
usage.success_count += 1
|
| 2212 |
+
|
| 2213 |
+
# CRASH! (no commit - simulating failure)
|
| 2214 |
+
|
| 2215 |
+
# Verify NEITHER persisted
|
| 2216 |
+
async with session_maker() as session:
|
| 2217 |
+
result = await session.execute(
|
| 2218 |
+
select(TestJobModel).where(TestJobModel.job_id == "atomic-job")
|
| 2219 |
+
)
|
| 2220 |
+
job = result.scalar_one()
|
| 2221 |
+
assert job.status == "queued", "Job should still be queued (transaction rolled back)"
|
| 2222 |
+
|
| 2223 |
+
result = await session.execute(
|
| 2224 |
+
select(TestApiKeyUsageModel).where(TestApiKeyUsageModel.key_index == 20)
|
| 2225 |
+
)
|
| 2226 |
+
usage = result.scalar_one()
|
| 2227 |
+
assert usage.total_requests == 0, "Usage should be unchanged (transaction rolled back)"
|
| 2228 |
+
|
| 2229 |
+
@pytest.mark.asyncio
|
| 2230 |
+
async def test_transaction_atomicity_both_persist_on_commit(self, integration_db):
|
| 2231 |
+
"""
|
| 2232 |
+
Test that job update and usage recording BOTH persist when we commit.
|
| 2233 |
+
"""
|
| 2234 |
+
session_maker = integration_db["session_maker"]
|
| 2235 |
+
|
| 2236 |
+
# Create job and usage
|
| 2237 |
+
async with session_maker() as session:
|
| 2238 |
+
job = TestJobModel(
|
| 2239 |
+
job_id="atomic-success",
|
| 2240 |
+
user_id="user",
|
| 2241 |
+
job_type="text",
|
| 2242 |
+
status="queued",
|
| 2243 |
+
priority="fast",
|
| 2244 |
+
created_at=datetime.utcnow()
|
| 2245 |
+
)
|
| 2246 |
+
usage = TestApiKeyUsageModel(
|
| 2247 |
+
key_index=21,
|
| 2248 |
+
total_requests=0,
|
| 2249 |
+
success_count=0,
|
| 2250 |
+
failure_count=0
|
| 2251 |
+
)
|
| 2252 |
+
session.add_all([job, usage])
|
| 2253 |
+
await session.commit()
|
| 2254 |
+
|
| 2255 |
+
# Process successfully
|
| 2256 |
+
async with session_maker() as session:
|
| 2257 |
+
# Update job
|
| 2258 |
+
result = await session.execute(
|
| 2259 |
+
select(TestJobModel).where(TestJobModel.job_id == "atomic-success")
|
| 2260 |
+
)
|
| 2261 |
+
job = result.scalar_one()
|
| 2262 |
+
job.status = "completed"
|
| 2263 |
+
job.completed_at = datetime.utcnow()
|
| 2264 |
+
|
| 2265 |
+
# Update usage
|
| 2266 |
+
result = await session.execute(
|
| 2267 |
+
select(TestApiKeyUsageModel).where(TestApiKeyUsageModel.key_index == 21)
|
| 2268 |
+
)
|
| 2269 |
+
usage = result.scalar_one()
|
| 2270 |
+
usage.total_requests += 1
|
| 2271 |
+
usage.success_count += 1
|
| 2272 |
+
|
| 2273 |
+
# COMMIT!
|
| 2274 |
+
await session.commit()
|
| 2275 |
+
|
| 2276 |
+
# Verify BOTH persisted
|
| 2277 |
+
async with session_maker() as session:
|
| 2278 |
+
result = await session.execute(
|
| 2279 |
+
select(TestJobModel).where(TestJobModel.job_id == "atomic-success")
|
| 2280 |
+
)
|
| 2281 |
+
job = result.scalar_one()
|
| 2282 |
+
assert job.status == "completed", "Job should be completed"
|
| 2283 |
+
|
| 2284 |
+
result = await session.execute(
|
| 2285 |
+
select(TestApiKeyUsageModel).where(TestApiKeyUsageModel.key_index == 21)
|
| 2286 |
+
)
|
| 2287 |
+
usage = result.scalar_one()
|
| 2288 |
+
assert usage.total_requests == 1, "Usage should be updated"
|
| 2289 |
+
|
| 2290 |
+
|
| 2291 |
if __name__ == "__main__":
|
| 2292 |
pytest.main([__file__, "-v"])
|
| 2293 |
|
| 2294 |
+
|