Artifact 8ba199787c40f27cc1f774836f69e1d9b2ad05522c2af25f3d8c52b3ab006a26:
- File
cli/repo_clone_test.go
— part of check-in
[a02c9572da]
at
2026-07-23 16:18:24
on branch trunk
— clone: add --timeout override and make the deadline reliably fire (#120) (#144)
The clone command hardcoded a 10-minute context deadline with no way to raise or disable it, and the deadline did not reliably interrupt a slow clone.
- RepoCloneCmd gains a --timeout flag (default 10m, matching the old behavior; 0 disables the deadline for very large repositories). cloneContext branches away from context.WithTimeout for a non-positive timeout -- WithTimeout(parent, 0) yields an already-expired deadline, which would abort instantly the very clone the flag exists to unbound.
- The whole-repository crosslink sweep now observes context cancellation. It runs after the exchange-round loop, so it had no round boundary of its own at which the deadline could fire -- on a large repository it could run long past the deadline uninterrupted. manifest.CrosslinkContext checks the context every 256 candidates; manifest.Crosslink stays a background-context wrapper so its ~30 existing callers are unchanged. processResponse gains the same batched check so a large single-round batch is interruptible too, and the sync session's and the serve handler's post-convergence sweeps are wired to their contexts for the same reason.
Each cancellation point is pinned by a test that fails without it: the crosslink and processResponse tests drive a context that survives the first poll and dies before the second, so only the batched check can observe it; the crosslink-sweep test cancels after a zero-card final round, so nothing before the sweep is left to notice.
Also adds a clone -> serve -> reclone fidelity regression guard (#114), built with the canonical fossil binary rather than this library's Commit. Commit does not deltify, so a corpus built through it holds zero delta rows and never reaches the serve side's deltified-row expansion branch -- a green result from such a corpus proves nothing about #114. The guard now asserts the source holds delta rows before measuring anything, and compares the second-generation clone's tip, artifact count, blob-store size, delta-row count and checked-out tree content against the source.
Measured against that corpus (71 artifacts, 54 delta rows), #114 does not reproduce: gen1 and gen2 both hold the same 54 delta rows, the same tip and artifact count, a 1.009 store ratio, identical checked-out content, and pass fossil test-integrity. The receive path re-deltifies what the serve side expanded, via content.Deltify on the crosslink mlink path, so the expansion the serve side performs does not survive into the store. (user: [email protected], size: 2022) [annotate] [blame] [check-ins using]
package cli_test import ( "testing" "time" "github.com/alecthomas/kong" "github.com/danmestas/go-libfossil/cli" _ "github.com/danmestas/go-libfossil/internal/testdriver" ) // cloneTestCLI mirrors the wiring in cmd/libfossil/main.go closely enough to // exercise kong flag parsing for RepoCloneCmd without depending on the main // package. type cloneTestCLI struct { cli.Globals Repo cli.RepoCmd `cmd:""` } // TestRepoCloneCmdDefaultTimeout pins the historical 10-minute deadline as the // default so an ordinary clone behaves exactly as before the flag existed. func TestRepoCloneCmdDefaultTimeout(t *testing.T) { var c cloneTestCLI parser, err := kong.New(&c) if err != nil { t.Fatalf("kong.New: %v", err) } if _, err := parser.Parse([]string{"repo", "clone", "http://x", "out.fossil"}); err != nil { t.Fatalf("parse: %v", err) } if c.Repo.Clone.Timeout != 10*time.Minute { t.Errorf("default Timeout = %v, want 10m", c.Repo.Clone.Timeout) } } // TestRepoCloneCmdTimeoutFlagOverride raises the deadline past the default so a // large repository can finish. func TestRepoCloneCmdTimeoutFlagOverride(t *testing.T) { var c cloneTestCLI parser, err := kong.New(&c) if err != nil { t.Fatalf("kong.New: %v", err) } if _, err := parser.Parse([]string{"repo", "clone", "--timeout", "45m", "http://x", "out.fossil"}); err != nil { t.Fatalf("parse: %v", err) } if c.Repo.Clone.Timeout != 45*time.Minute { t.Errorf("Timeout = %v, want 45m", c.Repo.Clone.Timeout) } } // TestRepoCloneCmdTimeoutDisable proves the deadline can be turned off entirely // with 0 for an arbitrarily long clone. func TestRepoCloneCmdTimeoutDisable(t *testing.T) { var c cloneTestCLI parser, err := kong.New(&c) if err != nil { t.Fatalf("kong.New: %v", err) } if _, err := parser.Parse([]string{"repo", "clone", "--timeout", "0", "http://x", "out.fossil"}); err != nil { t.Fatalf("parse: %v", err) } if c.Repo.Clone.Timeout != 0 { t.Errorf("Timeout = %v, want 0 (disabled)", c.Repo.Clone.Timeout) } }