Artifact 1b7231c23ff4fadeae933836fd20b316c1875907d4f44bf88e632abdcfa48a69:
- File
internal/sync/clone_bound_test.go
— part of check-in
[f1236bd031]
at
2026-07-23 13:54:08
on branch trunk
— xfer: select framing by Content-Type; bound clone rounds by client decode limit (#133)
* fix: select xfer framing by Content-Type (#106); bound clone rounds by the client's decode limit (#109)
seeing which parsed, which made a decompression failure indistinguishable from "wrong framing" (the root of #104). Select framing from the §4 Content-Type instead: application/x-fossil is the §4.1 compressed container, every other type (including application/x-fossil-uncompressed, used by clone-v3 replies) is plain card text — the same rule canonical fossil applies. Verified against fossil 2.28: pull replies arrive as the prefixed container, clone replies as plain text, and no reply is ever an unprefixed raw zlib stream. The former unprefixed-zlib framing, checked first on the unverified belief that fossil emits it, was removed along with the length-prefix/zlib-header aliasing guard that only existed to disambiguate the two compressed framings trial-dispatch conflated. A compressed body that fails to inflate is now reported as the fault it is, never re-fed to the card parser. xfer.Decode takes the Content-Type; byte-oriented callers with no header pass the compressed type explicitly.
crossing one whole, so a round carried budget + one whole artifact. When sub-budget filler preceded a large artifact, that sum exceeded what the client could decode (xfer.MaxDecompressedBytes) and the clone failed with a spurious "declared container length exceeds" error whose threshold depended on incidental filler size. The server now flushes the round and sends an over-bound artifact alone when adding it would cross the client's decode bound, so clonability is a property of the artifact alone. The 2*budget compile-time guard now certifies what it appears to.
* fix: reject unrecognized xfer Content-Type with 415 (#106)
A POST whose Content-Type was absent or unrecognized fell through to the plaintext card decoder, which is exactly the #104 failure shape: possibly- compressed binary fed to the card parser. Request bodies come from untrusted peers, so a body carrying no framing signal is rejected rather than guessed at. Real fossil always sets the header; no conforming peer is affected.
xfer.KnownContentType shares the media-type parsing with the framing check, so the server and Decode agree on what a type means.
Also runs the #109 filler subtests in parallel — serially they pushed internal/sync past the 120s per-package timeout CI uses. (user: [email protected], size: 3343) [annotate] [blame] [check-ins using]
package sync_test import ( "fmt" "testing" "github.com/danmestas/go-libfossil/internal/blob" "github.com/danmestas/go-libfossil/internal/sync" "github.com/danmestas/go-libfossil/internal/xfer" ) // TestCloneSelfRoundTripLargeArtifactAfterFiller is the #109 regression test: // a large artifact must clone even when ordinary filler precedes it in the // same round, because the true ceiling on a round is the client's decode bound // minus whatever the round already accumulated -- not the bound alone. // // Method: for each filler size, a fresh source holds one incompressible filler // blob (stored first, so it takes the lower rid and the clone sends it ahead of // the artifact) followed by the 58.7 MB artifact measured in the issue. The // repository is served and cloned by this implementation over real HTTP, so the // client decodes every round through xfer.MaxDecompressedBytes; a round that // carried filler plus the whole artifact would exceed that bound and fail the // clone. cloneSelfAndCompare requires artifact parity. // // The three filler sizes are exactly the issue's: each is under the round // budget, so before the fix the filler did not exhaust the budget and the // artifact rode into the same round. The non-monotonic 17 MB case that passed // (by exhausting the budget and pushing the artifact into its own round) is not // retested here -- the point is that clonability no longer depends on incidental // filler size at all. func TestCloneSelfRoundTripLargeArtifactAfterFiller(t *testing.T) { if testing.Short() { t.Skip("allocates a 58.7 MB artifact behind filler at three sizes") } // The exact artifact size from issue #109. const artifactBytes = 58720256 if artifactBytes >= xfer.MaxDecompressedBytes { t.Fatalf("artifact of %d bytes cannot fit under the decode bound of %d when sent alone", artifactBytes, xfer.MaxDecompressedBytes) } for _, fillerBytes := range []int{8 << 20, 12 << 20, 15 << 20} { t.Run(fmt.Sprintf("filler-%dMiB", fillerBytes>>20), func(t *testing.T) { // Each size is an independent repository in its own temp dir, so the // three run concurrently -- serially they push this package past the // 120s per-package timeout CI runs with. t.Parallel() // The filler must stay under the batch budget so it does not force a // round of its own; only then does the artifact ride into the same // round, which is the #109 condition. (filler + artifact + per-card // overhead exceeds the decode bound at each of these sizes -- proven // by the clone itself, which is the real assertion here.) if fillerBytes >= sync.DefaultCloneBatchBytes { t.Fatalf("filler %d must stay under the batch budget %d to reproduce #109", fillerBytes, sync.DefaultCloneBatchBytes) } dir := t.TempDir() srcRepo := newSelfRoundTripRepo(t, dir) defer srcRepo.Close() // Store filler first so it takes the lower rid; the clone iterates by // rid, so this places the artifact behind accumulated round bytes. if _, _, err := blob.Store(srcRepo.DB(), incompressibleBytes(2, fillerBytes)); err != nil { t.Fatalf("store filler: %v", err) } if _, _, err := blob.Store(srcRepo.DB(), incompressibleBytes(3, artifactBytes)); err != nil { t.Fatalf("store artifact: %v", err) } cloneSelfAndCompare(t, srcRepo, dir) }) } }