gabriel / musehub public

test_repair_commit_body_size_limit.py file-level

at sha256:1 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 πŸ’₯ blast risk
sha256:1 fix: getting-started page's muse --version sample output showed musehub… · gabriel · Oct 23, 2026
1 """musehub#228 β€” repair-commit (and repair-object/repair-snapshot) need their
2 own size-limit tier, distinct from the generic 10 MB API cap and the 500 MB
3 push cap.
4
5 Background: repair-commit's payload is a single WireCommit record (commit
6 metadata + structured_delta), not a full mpack. It was reasonably assumed to
7 be small when ContentSizeLimitMiddleware was written, but a commit with an
8 unusually large structured diff (observed: 15.6 MB on a real bulk-refactor
9 commit, gabriel/muse sha256:2f17e90958a3a39216322972274314b4c495ed995ea0edf4d24a777d171226f7)
10 breaks that assumption and gets rejected with 413 before the handler ever runs.
11
12 Phase 1 audit found repair-object and repair-snapshot share the exact same
13 exposure β€” repair-object carries arbitrary raw object bytes (fundamentally
14 unbounded, same risk profile as a push), repair-snapshot carries a manifest
15 that can exceed 10 MB for a repo with enough tracked files.
16
17 Design (confirmed with gabriel): three tiers.
18 PUSH_MAX_BYTES (500 MB) β€” push/*, repair-object (raw bytes, push-scale)
19 REPAIR_MAX_BYTES (100 MB) β€” repair-commit, repair-snapshot (metadata, ~6x
20 headroom over the observed 15.6 MB worst case)
21 API_MAX_BYTES ( 10 MB) β€” everything else, unchanged
22 """
23 from __future__ import annotations
24
25 from httpx import AsyncClient
26
27 from musehub.middleware.content_size import API_MAX_BYTES, PUSH_MAX_BYTES, REPAIR_MAX_BYTES
28
29
30 def test_limits_are_correct_values() -> None:
31 assert API_MAX_BYTES == 10 * 1024 * 1024
32 assert REPAIR_MAX_BYTES == 100 * 1024 * 1024
33 assert PUSH_MAX_BYTES == 500 * 1024 * 1024
34 assert API_MAX_BYTES < REPAIR_MAX_BYTES < PUSH_MAX_BYTES
35
36
37 # ---------------------------------------------------------------------------
38 # repair-commit β€” the ticket's reproduction case
39 # ---------------------------------------------------------------------------
40
41
42 async def test_repair_commit_15_6mb_structured_delta_not_rejected_by_middleware(
43 client: AsyncClient,
44 ) -> None:
45 """The exact real-world case: a commit whose structured_delta is ~15.6 MB
46 β€” well over the old 10 MB API cap, comfortably under the new 100 MB
47 repair tier β€” must not be rejected at the middleware layer.
48 """
49 observed_size = int(15.6 * 1024 * 1024)
50 resp = await client.post(
51 "/gabriel/muse/repair-commit",
52 headers={"Content-Length": str(observed_size)},
53 content=b"",
54 )
55 # No auth / no real body β†’ 401 or 422 from the handler, but must get PAST
56 # the middleware β€” never 413.
57 assert resp.status_code != 413
58
59
60 async def test_repair_commit_over_api_limit_but_under_repair_limit_allowed(
61 client: AsyncClient,
62 ) -> None:
63 """20 MB on repair-commit (over the old 10 MB API cap) must not 413."""
64 over_api_limit = API_MAX_BYTES * 2
65 resp = await client.post(
66 "/gabriel/muse/repair-commit",
67 headers={"Content-Length": str(over_api_limit)},
68 content=b"",
69 )
70 assert resp.status_code != 413
71
72
73 async def test_repair_commit_over_repair_limit_still_rejected(
74 client: AsyncClient,
75 ) -> None:
76 """The ceiling isn't removed entirely β€” a payload genuinely over 100 MB
77 on repair-commit must still get 413."""
78 over_repair_limit = REPAIR_MAX_BYTES + 1
79 resp = await client.post(
80 "/gabriel/muse/repair-commit",
81 headers={"Content-Length": str(over_repair_limit)},
82 content=b"",
83 )
84 assert resp.status_code == 413
85 assert "too large" in resp.json()["detail"].lower()
86
87
88 # ---------------------------------------------------------------------------
89 # repair-snapshot β€” same tier, found during the Phase 1 audit
90 # ---------------------------------------------------------------------------
91
92
93 async def test_repair_snapshot_over_api_limit_but_under_repair_limit_allowed(
94 client: AsyncClient,
95 ) -> None:
96 over_api_limit = API_MAX_BYTES * 2
97 resp = await client.post(
98 "/gabriel/muse/repair-snapshot",
99 headers={"Content-Length": str(over_api_limit)},
100 content=b"",
101 )
102 assert resp.status_code != 413
103
104
105 async def test_repair_snapshot_over_repair_limit_still_rejected(
106 client: AsyncClient,
107 ) -> None:
108 over_repair_limit = REPAIR_MAX_BYTES + 1
109 resp = await client.post(
110 "/gabriel/muse/repair-snapshot",
111 headers={"Content-Length": str(over_repair_limit)},
112 content=b"",
113 )
114 assert resp.status_code == 413
115
116
117 # ---------------------------------------------------------------------------
118 # repair-object β€” joins the PUSH tier (raw bytes, push-scale), not REPAIR
119 # ---------------------------------------------------------------------------
120
121
122 async def test_repair_object_over_repair_limit_but_under_push_limit_allowed(
123 client: AsyncClient,
124 ) -> None:
125 """repair-object carries raw object bytes β€” it must use the 500 MB push
126 tier, not the 100 MB repair-metadata tier. A 150 MB payload (over
127 REPAIR_MAX_BYTES, under PUSH_MAX_BYTES) must not 413."""
128 over_repair_limit = REPAIR_MAX_BYTES + (50 * 1024 * 1024)
129 resp = await client.post(
130 "/gabriel/muse/repair-object",
131 headers={"Content-Length": str(over_repair_limit)},
132 content=b"",
133 )
134 assert resp.status_code != 413
135
136
137 async def test_repair_object_over_push_limit_still_rejected(
138 client: AsyncClient,
139 ) -> None:
140 over_push_limit = PUSH_MAX_BYTES + 1
141 resp = await client.post(
142 "/gabriel/muse/repair-object",
143 headers={"Content-Length": str(over_push_limit)},
144 content=b"",
145 )
146 assert resp.status_code == 413