test_wire_mpack_unpack_step3_tiers4567.py
python
sha256:ad616c6113d6c00f4efed6b2993734ca46d3e9b5bee25addd4ce8ae6b57136e5
chore: bump version to 0.2.0rc11; typing audit clean + all …
Sonnet 4.6
minor
⚠ breaking
55 days ago
| 1 | """Push Protocol Step 3 (unpack-mpack) — Tiers 4–7: stress, integrity, performance, security. |
| 2 | |
| 3 | Tier 4 — Stress: concurrent wire_push_unpack_mpack calls all land without errors. |
| 4 | Tier 5 — Data integrity: DB invariants on MusehubBackgroundJob and MusehubBranch. |
| 5 | Tier 6 — Performance: unpack endpoint latency gate. |
| 6 | Tier 7 — Security: malformed keys, oversized keys, unauthenticated access. |
| 7 | """ |
| 8 | from __future__ import annotations |
| 9 | |
| 10 | import asyncio |
| 11 | import time |
| 12 | |
| 13 | import msgpack |
| 14 | import pytest |
| 15 | import pytest_asyncio |
| 16 | from httpx import AsyncClient, ASGITransport |
| 17 | from sqlalchemy import select, func, text |
| 18 | from sqlalchemy.ext.asyncio import AsyncSession |
| 19 | |
| 20 | from muse.core.mpack import build_wire_mpack |
| 21 | from muse.core.types import blob_id, fake_id |
| 22 | from musehub.auth.dependencies import require_valid_token |
| 23 | from musehub.auth.request_signing import MSignContext |
| 24 | from musehub.config import get_settings |
| 25 | from musehub.core.genesis import compute_identity_id |
| 26 | from musehub.db.database import get_db |
| 27 | from musehub.db.musehub_jobs_models import MusehubBackgroundJob |
| 28 | from musehub.db.musehub_repo_models import MusehubBranch |
| 29 | from musehub.main import app |
| 30 | from musehub.services.musehub_repository import create_repo |
| 31 | |
| 32 | _OWNER = "gabriel" |
| 33 | _IDENTITY_ID = compute_identity_id(b"gabriel") |
| 34 | _OTHER_IDENTITY_ID = compute_identity_id(b"aria") |
| 35 | _REPO_NAME = "step3-tiers-test" |
| 36 | |
| 37 | _MPACK_BYTES = build_wire_mpack({"objects": [], "commits": [], "snapshots": []}) |
| 38 | _MPACK_KEY = blob_id(_MPACK_BYTES) |
| 39 | _HEAD = fake_id("step3-tiers-tip") |
| 40 | |
| 41 | _AUTH_CTX = MSignContext( |
| 42 | handle=_OWNER, |
| 43 | identity_id=_IDENTITY_ID, |
| 44 | is_agent=False, |
| 45 | is_admin=False, |
| 46 | ) |
| 47 | |
| 48 | |
| 49 | # --------------------------------------------------------------------------- |
| 50 | # Fixtures |
| 51 | # --------------------------------------------------------------------------- |
| 52 | |
| 53 | @pytest_asyncio.fixture() |
| 54 | async def repo(db_session: AsyncSession): |
| 55 | r = await create_repo( |
| 56 | db_session, |
| 57 | name=_REPO_NAME, |
| 58 | owner=_OWNER, |
| 59 | owner_user_id=_IDENTITY_ID, |
| 60 | visibility="public", |
| 61 | initialize=False, |
| 62 | ) |
| 63 | await db_session.commit() |
| 64 | return r |
| 65 | |
| 66 | |
| 67 | @pytest_asyncio.fixture(autouse=True) |
| 68 | async def mock_get_mpack(): |
| 69 | from unittest.mock import AsyncMock, MagicMock, patch |
| 70 | mock_backend = MagicMock() |
| 71 | mock_backend.get_mpack = AsyncMock(return_value=_MPACK_BYTES) |
| 72 | with patch("musehub.services.musehub_wire.get_backend", return_value=mock_backend), \ |
| 73 | patch("musehub.services.musehub_wire_push.get_backend", return_value=mock_backend): |
| 74 | yield mock_backend |
| 75 | |
| 76 | |
| 77 | def _make_client(db_session: AsyncSession, auth_ctx: MSignContext = _AUTH_CTX): |
| 78 | async def _override_db(): |
| 79 | yield db_session |
| 80 | app.dependency_overrides[get_db] = _override_db |
| 81 | app.dependency_overrides[require_valid_token] = lambda: auth_ctx |
| 82 | return AsyncClient(transport=ASGITransport(app=app), base_url="https://localhost:1337") |
| 83 | |
| 84 | |
| 85 | def _body( |
| 86 | mpack_key: str = _MPACK_KEY, |
| 87 | branch: str = "main", |
| 88 | head: str = "", |
| 89 | commits_count: int = 1, |
| 90 | objects_count: int = 2, |
| 91 | ) -> bytes: |
| 92 | return msgpack.packb( |
| 93 | { |
| 94 | "mpack_key": mpack_key, |
| 95 | "branch": branch, |
| 96 | "head": head, |
| 97 | "commits_count": commits_count, |
| 98 | "objects_count": objects_count, |
| 99 | }, |
| 100 | use_bin_type=True, |
| 101 | ) |
| 102 | |
| 103 | |
| 104 | async def _unpack(client: AsyncClient, repo_name: str = _REPO_NAME, **kwargs) -> int: |
| 105 | resp = await client.post( |
| 106 | f"/{_OWNER}/{repo_name}/push/unpack-mpack", |
| 107 | content=_body(**kwargs), |
| 108 | headers={"Content-Type": "application/x-msgpack"}, |
| 109 | ) |
| 110 | return resp.status_code |
| 111 | |
| 112 | |
| 113 | # --------------------------------------------------------------------------- |
| 114 | # Tier 4 — Stress |
| 115 | # --------------------------------------------------------------------------- |
| 116 | |
| 117 | @pytest.mark.tier4 |
| 118 | @pytest.mark.asyncio |
| 119 | async def test_t4_concurrent_unpack_calls_all_succeed( |
| 120 | repo, session_factory, |
| 121 | ) -> None: |
| 122 | """20 concurrent wire_push_unpack_mpack calls with distinct mpack_keys all succeed. |
| 123 | |
| 124 | Tests inline processing under concurrency — every call must complete without |
| 125 | deadlocking. Phase 3: all processing is inline; no background job rows. |
| 126 | """ |
| 127 | from musehub.services.musehub_wire import wire_push_unpack_mpack |
| 128 | |
| 129 | n = 20 |
| 130 | |
| 131 | async def _do(i: int) -> None: |
| 132 | # Each call gets a unique mpack_key so job_id is unique |
| 133 | content = f"object-{i}".encode() |
| 134 | oid = blob_id(content) |
| 135 | unique_bytes = build_wire_mpack( |
| 136 | {"objects": [{"object_id": oid, "content": content}], "commits": [], "snapshots": []} |
| 137 | ) |
| 138 | unique_key = blob_id(unique_bytes) |
| 139 | async with session_factory() as sess: |
| 140 | # Override get_mpack to return the correct bytes for this key |
| 141 | from unittest.mock import AsyncMock, MagicMock, patch |
| 142 | mock_backend = MagicMock() |
| 143 | mock_backend.get_mpack = AsyncMock(return_value=unique_bytes) |
| 144 | with patch("musehub.services.musehub_wire.get_backend", return_value=mock_backend), \ |
| 145 | patch("musehub.services.musehub_wire_push.get_backend", return_value=mock_backend): |
| 146 | await wire_push_unpack_mpack( |
| 147 | sess, repo.repo_id, unique_key, _OWNER, |
| 148 | branch="main", head_commit_id="", commits_count=1, objects_count=1, |
| 149 | ) |
| 150 | await sess.commit() |
| 151 | |
| 152 | # If any call raises, gather re-raises immediately — no further assertion needed. |
| 153 | await asyncio.gather(*[_do(i) for i in range(n)]) |
| 154 | |
| 155 | |
| 156 | @pytest.mark.tier4 |
| 157 | @pytest.mark.asyncio |
| 158 | async def test_t4_concurrent_branch_updates_for_same_branch( |
| 159 | repo, session_factory, |
| 160 | ) -> None: |
| 161 | """10 concurrent calls advancing the same branch serialise without data corruption. |
| 162 | |
| 163 | The service uses WITH FOR UPDATE on the branch row — concurrent updates |
| 164 | must not produce two branch rows for the same name. Pre-create the branch |
| 165 | so all concurrent calls hit the UPDATE path (avoiding the INSERT race). |
| 166 | """ |
| 167 | from musehub.services.musehub_wire import wire_push_unpack_mpack |
| 168 | from musehub.core.genesis import compute_branch_id |
| 169 | |
| 170 | # Pre-create the branch so concurrent calls all SELECT-for-update and UPDATE |
| 171 | async with session_factory() as seed_sess: |
| 172 | seed_sess.add(MusehubBranch( |
| 173 | branch_id=compute_branch_id(repo.repo_id, "concurrent-branch"), |
| 174 | repo_id=repo.repo_id, |
| 175 | name="concurrent-branch", |
| 176 | head_commit_id=fake_id("seed-head"), |
| 177 | )) |
| 178 | await seed_sess.commit() |
| 179 | |
| 180 | n = 10 |
| 181 | |
| 182 | async def _do(i: int) -> None: |
| 183 | content = f"seq-{i}".encode() |
| 184 | oid = blob_id(content) |
| 185 | unique_bytes = build_wire_mpack( |
| 186 | {"objects": [{"object_id": oid, "content": content}], "commits": [], "snapshots": []} |
| 187 | ) |
| 188 | unique_key = blob_id(unique_bytes) |
| 189 | head = fake_id(f"commit-{i}") |
| 190 | async with session_factory() as sess: |
| 191 | from unittest.mock import AsyncMock, MagicMock, patch |
| 192 | mock_backend = MagicMock() |
| 193 | mock_backend.get_mpack = AsyncMock(return_value=unique_bytes) |
| 194 | with patch("musehub.services.musehub_wire.get_backend", return_value=mock_backend), \ |
| 195 | patch("musehub.services.musehub_wire_push.get_backend", return_value=mock_backend): |
| 196 | await wire_push_unpack_mpack( |
| 197 | sess, repo.repo_id, unique_key, _OWNER, |
| 198 | branch="concurrent-branch", head_commit_id=head, |
| 199 | commits_count=1, objects_count=0, force=True, |
| 200 | ) |
| 201 | await sess.commit() |
| 202 | |
| 203 | await asyncio.gather(*[_do(i) for i in range(n)]) |
| 204 | |
| 205 | async with session_factory() as check_sess: |
| 206 | result = await check_sess.execute( |
| 207 | select(func.count()).select_from(MusehubBranch).where( |
| 208 | MusehubBranch.repo_id == repo.repo_id, |
| 209 | MusehubBranch.name == "concurrent-branch", |
| 210 | ) |
| 211 | ) |
| 212 | count = result.scalar() |
| 213 | |
| 214 | assert count == 1, f"expected exactly 1 branch row, got {count}" |
| 215 | |
| 216 | |
| 217 | # --------------------------------------------------------------------------- |
| 218 | # Tier 5 — Data integrity |
| 219 | # --------------------------------------------------------------------------- |
| 220 | |
| 221 | @pytest.mark.tier5 |
| 222 | @pytest.mark.asyncio |
| 223 | async def test_t5_unpack_returns_correct_shape( |
| 224 | db_session: AsyncSession, repo, |
| 225 | ) -> None: |
| 226 | """Successful inline unpack returns the expected response keys (Phase 3 — no background job).""" |
| 227 | from musehub.services.musehub_wire import wire_push_unpack_mpack |
| 228 | |
| 229 | result = await wire_push_unpack_mpack( |
| 230 | db_session, repo.repo_id, _MPACK_KEY, _OWNER, |
| 231 | branch="main", head_commit_id="", commits_count=3, objects_count=7, |
| 232 | ) |
| 233 | |
| 234 | assert "head" in result |
| 235 | assert "branch" in result |
| 236 | assert result["branch"] == "main" |
| 237 | assert "objects_in_mpack" in result |
| 238 | assert "commits_in_mpack" in result |
| 239 | |
| 240 | |
| 241 | @pytest.mark.tier5 |
| 242 | @pytest.mark.asyncio |
| 243 | async def test_t5_branch_row_created_when_head_provided( |
| 244 | db_session: AsyncSession, repo, |
| 245 | ) -> None: |
| 246 | """Branch row is inserted when head_commit_id is provided and branch doesn't exist.""" |
| 247 | from musehub.services.musehub_wire import wire_push_unpack_mpack |
| 248 | |
| 249 | await wire_push_unpack_mpack( |
| 250 | db_session, repo.repo_id, _MPACK_KEY, _OWNER, |
| 251 | branch="feat/new-branch", head_commit_id=_HEAD, |
| 252 | commits_count=1, objects_count=0, |
| 253 | ) |
| 254 | |
| 255 | result = await db_session.execute( |
| 256 | select(MusehubBranch).where( |
| 257 | MusehubBranch.repo_id == repo.repo_id, |
| 258 | MusehubBranch.name == "feat/new-branch", |
| 259 | ) |
| 260 | ) |
| 261 | branch_row = result.scalar_one_or_none() |
| 262 | assert branch_row is not None, "branch row was not created" |
| 263 | assert branch_row.head_commit_id == _HEAD |
| 264 | |
| 265 | |
| 266 | @pytest.mark.tier5 |
| 267 | @pytest.mark.asyncio |
| 268 | async def test_t5_branch_row_updated_when_branch_exists( |
| 269 | db_session: AsyncSession, repo, |
| 270 | ) -> None: |
| 271 | """When branch already exists, unpack updates head_commit_id — no duplicate row.""" |
| 272 | from musehub.services.musehub_wire import wire_push_unpack_mpack |
| 273 | |
| 274 | first_head = fake_id("first-head") |
| 275 | second_head = fake_id("second-head") |
| 276 | |
| 277 | await wire_push_unpack_mpack( |
| 278 | db_session, repo.repo_id, _MPACK_KEY, _OWNER, |
| 279 | branch="main", head_commit_id=first_head, |
| 280 | commits_count=1, objects_count=0, |
| 281 | ) |
| 282 | |
| 283 | # Second push to same branch with different mpack (different key) |
| 284 | second_content = b"second-push-content" |
| 285 | second_oid = blob_id(second_content) |
| 286 | second_bytes = build_wire_mpack( |
| 287 | {"objects": [{"object_id": second_oid, "content": second_content}], "commits": [], "snapshots": []} |
| 288 | ) |
| 289 | second_key = blob_id(second_bytes) |
| 290 | from unittest.mock import AsyncMock, MagicMock, patch |
| 291 | mock_backend = MagicMock() |
| 292 | mock_backend.get_mpack = AsyncMock(return_value=second_bytes) |
| 293 | with patch("musehub.services.musehub_wire.get_backend", return_value=mock_backend), \ |
| 294 | patch("musehub.services.musehub_wire_push.get_backend", return_value=mock_backend): |
| 295 | await wire_push_unpack_mpack( |
| 296 | db_session, repo.repo_id, second_key, _OWNER, |
| 297 | branch="main", head_commit_id=second_head, |
| 298 | commits_count=1, objects_count=0, force=True, |
| 299 | ) |
| 300 | |
| 301 | result = await db_session.execute( |
| 302 | select(func.count()).select_from(MusehubBranch).where( |
| 303 | MusehubBranch.repo_id == repo.repo_id, |
| 304 | MusehubBranch.name == "main", |
| 305 | ) |
| 306 | ) |
| 307 | assert result.scalar() == 1, "duplicate branch row created" |
| 308 | |
| 309 | branch_result = await db_session.execute( |
| 310 | select(MusehubBranch.head_commit_id).where( |
| 311 | MusehubBranch.repo_id == repo.repo_id, |
| 312 | MusehubBranch.name == "main", |
| 313 | ) |
| 314 | ) |
| 315 | assert branch_result.scalar() == second_head, "branch head not updated to second push" |
| 316 | |
| 317 | |
| 318 | @pytest.mark.tier5 |
| 319 | @pytest.mark.asyncio |
| 320 | async def test_t5_job_row_not_written_on_mpack_not_found( |
| 321 | db_session: AsyncSession, repo, |
| 322 | ) -> None: |
| 323 | """When MinIO returns None, ValueError is raised and no job row is persisted.""" |
| 324 | from musehub.services.musehub_wire import wire_push_unpack_mpack |
| 325 | from unittest.mock import AsyncMock, MagicMock, patch |
| 326 | |
| 327 | mock_backend = MagicMock() |
| 328 | mock_backend.get_mpack = AsyncMock(return_value=None) |
| 329 | with patch("musehub.services.musehub_wire.get_backend", return_value=mock_backend), \ |
| 330 | patch("musehub.services.musehub_wire_push.get_backend", return_value=mock_backend): |
| 331 | with pytest.raises(ValueError): |
| 332 | await wire_push_unpack_mpack( |
| 333 | db_session, repo.repo_id, _MPACK_KEY, _OWNER, |
| 334 | branch="main", head_commit_id="", commits_count=0, objects_count=0, |
| 335 | ) |
| 336 | |
| 337 | result = await db_session.execute( |
| 338 | select(func.count()).select_from(MusehubBackgroundJob).where( |
| 339 | MusehubBackgroundJob.repo_id == repo.repo_id, |
| 340 | MusehubBackgroundJob.job_type == "mpack.index", |
| 341 | ) |
| 342 | ) |
| 343 | assert result.scalar() == 0, "job row written despite mpack not found" |
| 344 | |
| 345 | |
| 346 | # --------------------------------------------------------------------------- |
| 347 | # Tier 6 — Performance |
| 348 | # --------------------------------------------------------------------------- |
| 349 | |
| 350 | @pytest.mark.tier6 |
| 351 | @pytest.mark.asyncio |
| 352 | async def test_t6_unpack_latency_under_100ms(db_session: AsyncSession, repo) -> None: |
| 353 | """Unpack endpoint (stub MinIO, small mpack) completes in < 100ms.""" |
| 354 | async with _make_client(db_session) as client: |
| 355 | # Warm-up — exclude connection setup from gate |
| 356 | await client.post( |
| 357 | f"/{_OWNER}/{_REPO_NAME}/push/unpack-mpack", |
| 358 | content=_body(), |
| 359 | headers={"Content-Type": "application/x-msgpack"}, |
| 360 | ) |
| 361 | |
| 362 | t0 = time.perf_counter() |
| 363 | resp = await client.post( |
| 364 | f"/{_OWNER}/{_REPO_NAME}/push/unpack-mpack", |
| 365 | content=_body(commits_count=3, objects_count=10), |
| 366 | headers={"Content-Type": "application/x-msgpack"}, |
| 367 | ) |
| 368 | elapsed_ms = (time.perf_counter() - t0) * 1000 |
| 369 | |
| 370 | app.dependency_overrides.clear() |
| 371 | assert resp.status_code == 200 |
| 372 | assert elapsed_ms < 100, f"unpack took {elapsed_ms:.1f}ms — gate is 100ms" |
| 373 | |
| 374 | |
| 375 | @pytest.mark.tier6 |
| 376 | @pytest.mark.asyncio |
| 377 | async def test_t6_job_index_query_uses_index(db_session: AsyncSession, repo) -> None: |
| 378 | """EXPLAIN on the pending job query references ix_musehub_background_jobs_status_created.""" |
| 379 | plan = await db_session.execute(text( |
| 380 | "EXPLAIN SELECT * FROM musehub_background_jobs " |
| 381 | "WHERE status = 'pending' " |
| 382 | "ORDER BY created_at " |
| 383 | "LIMIT 10" |
| 384 | )) |
| 385 | plan_text = "\n".join(row[0] for row in plan) |
| 386 | assert "Index" in plan_text or "index" in plan_text, ( |
| 387 | f"job queue query not using an index:\n{plan_text}" |
| 388 | ) |
| 389 | |
| 390 | |
| 391 | # --------------------------------------------------------------------------- |
| 392 | # Tier 7 — Security |
| 393 | # --------------------------------------------------------------------------- |
| 394 | |
| 395 | @pytest.mark.tier7 |
| 396 | @pytest.mark.asyncio |
| 397 | async def test_t7_malformed_mpack_key_rejected(db_session: AsyncSession, repo) -> None: |
| 398 | """mpack_key without sha256: prefix is rejected with 422.""" |
| 399 | body = msgpack.packb( |
| 400 | {"mpack_key": "not-a-real-key", "branch": "main", "commits_count": 1, "objects_count": 0}, |
| 401 | use_bin_type=True, |
| 402 | ) |
| 403 | async with _make_client(db_session) as client: |
| 404 | resp = await client.post( |
| 405 | f"/{_OWNER}/{_REPO_NAME}/push/unpack-mpack", |
| 406 | content=body, |
| 407 | headers={"Content-Type": "application/x-msgpack"}, |
| 408 | ) |
| 409 | app.dependency_overrides.clear() |
| 410 | assert resp.status_code == 422, f"expected 422 for malformed key, got {resp.status_code}" |
| 411 | |
| 412 | |
| 413 | @pytest.mark.tier7 |
| 414 | @pytest.mark.asyncio |
| 415 | async def test_t7_oversized_key_field_rejected(db_session: AsyncSession, repo) -> None: |
| 416 | """A pathologically long mpack_key string is rejected, not stored.""" |
| 417 | body = msgpack.packb( |
| 418 | {"mpack_key": "sha256:" + "a" * 10_000, "branch": "main"}, |
| 419 | use_bin_type=True, |
| 420 | ) |
| 421 | async with _make_client(db_session) as client: |
| 422 | resp = await client.post( |
| 423 | f"/{_OWNER}/{_REPO_NAME}/push/unpack-mpack", |
| 424 | content=body, |
| 425 | headers={"Content-Type": "application/x-msgpack"}, |
| 426 | ) |
| 427 | app.dependency_overrides.clear() |
| 428 | assert resp.status_code in (400, 422), ( |
| 429 | f"expected 4xx for oversized key, got {resp.status_code}" |
| 430 | ) |
| 431 | |
| 432 | |
| 433 | @pytest.mark.tier7 |
| 434 | @pytest.mark.asyncio |
| 435 | async def test_t7_unauthenticated_request_rejected(db_session: AsyncSession, repo) -> None: |
| 436 | """No auth header → 401/403 before any job enqueue logic runs.""" |
| 437 | async def _override_db(): |
| 438 | yield db_session |
| 439 | app.dependency_overrides[get_db] = _override_db |
| 440 | # no require_valid_token override — real enforcement |
| 441 | |
| 442 | async with AsyncClient( |
| 443 | transport=ASGITransport(app=app), |
| 444 | base_url="https://localhost:1337", |
| 445 | ) as client: |
| 446 | resp = await client.post( |
| 447 | f"/{_OWNER}/{_REPO_NAME}/push/unpack-mpack", |
| 448 | content=_body(), |
| 449 | headers={"Content-Type": "application/x-msgpack"}, |
| 450 | ) |
| 451 | app.dependency_overrides.clear() |
| 452 | |
| 453 | assert resp.status_code in (401, 403) |
| 454 | |
| 455 | # No job row must exist — auth rejected before any DB write |
| 456 | result = await db_session.execute( |
| 457 | select(func.count()).select_from(MusehubBackgroundJob).where( |
| 458 | MusehubBackgroundJob.repo_id == repo.repo_id, |
| 459 | MusehubBackgroundJob.job_type == "mpack.index", |
| 460 | ) |
| 461 | ) |
| 462 | assert result.scalar() == 0, "job row written for unauthenticated request" |
| 463 | |
| 464 | |
| 465 | @pytest.mark.tier7 |
| 466 | @pytest.mark.asyncio |
| 467 | async def test_t7_commits_count_overflow_rejected(db_session: AsyncSession, repo) -> None: |
| 468 | """commits_count wildly exceeding the limit is rejected with 422 — no job enqueued.""" |
| 469 | settings = get_settings() |
| 470 | body = msgpack.packb( |
| 471 | { |
| 472 | "mpack_key": _MPACK_KEY, |
| 473 | "branch": "main", |
| 474 | "commits_count": settings.mpack_max_commits * 100, |
| 475 | "objects_count": 0, |
| 476 | }, |
| 477 | use_bin_type=True, |
| 478 | ) |
| 479 | async with _make_client(db_session) as client: |
| 480 | resp = await client.post( |
| 481 | f"/{_OWNER}/{_REPO_NAME}/push/unpack-mpack", |
| 482 | content=body, |
| 483 | headers={"Content-Type": "application/x-msgpack"}, |
| 484 | ) |
| 485 | app.dependency_overrides.clear() |
| 486 | assert resp.status_code == 422 |
| 487 | |
| 488 | result = await db_session.execute( |
| 489 | select(func.count()).select_from(MusehubBackgroundJob).where( |
| 490 | MusehubBackgroundJob.repo_id == repo.repo_id, |
| 491 | ) |
| 492 | ) |
| 493 | assert result.scalar() == 0, "job row written despite over-limit commits_count" |
File History
2 commits
sha256:ad616c6113d6c00f4efed6b2993734ca46d3e9b5bee25addd4ce8ae6b57136e5
chore: bump version to 0.2.0rc11; typing audit clean + all …
Sonnet 4.6
minor
⚠
55 days ago
sha256:ef10830ce231e0a20efcb0e2586cb879471247e916616e6fdd0d51df459e2595
fix: typing audit — 0 violations, 0 untyped defs across all…
Sonnet 4.6
minor
⚠
57 days ago