Artifact bae4b8ef1753c2326ee56b24531db40819849e9f4bd2a2190f57778288106231:
- File
hub/commit_perm_time_test.go
— part of check-in
[ad1d55f5b9]
at
2026-05-04 22:05:56
on branch trunk
— feat: close three gaps blocking bones go.mod libfossil drop (#137) (#138)
* feat(leaf/agent): clone-if-absent via Config.CloneFromHubURL (#137 gap A)
Adds agent.Config.CloneFromHubURL — when set and Config.RepoPath does not yet exist on disk, agent.New clones the leaf repo from the hub HTTP URL before opening it. Matching project-codes between leaf and hub are required for downstream NATS sync subjects to align, so callers that need to bootstrap a leaf against an existing hub no longer have to import libfossil directly for the pre-agent clone step.
Also exposes agent.CloneRepo(ctx, repoPath, hubURL) as a standalone helper for callers that need a clone independent of agent construction.
Existing behavior is preserved: when CloneFromHubURL is empty, agent.New errors on missing RepoPath as before (covered by TestAgentNew_RejectsMissingRepoWithoutCloneURL).
Closes part of #137 (gap A — bones internal/coord/leaf.go).
* feat(hub): subscribe to fossil-sync NATS subject in NewHub (#137 gap B)
NewHub now connects a local NATS client to its embedded server and subscribes to "fossil.<project-code>.sync" so peer leaf agents can push xfer requests to the hub over NATS — alongside the existing HTTP path. Previously, achieving this required running a leaf-agent in serve mode on top of the hub (Pull=false, Push=false, Autosync=AutosyncOff, ServeNATSEnabled=true), which transitively pulled libfossil into the consumer's import graph.
The subscriber is enabled by default. Read-only HTTP-only mirrors that shouldn't accept NATS pushes can opt out via Config.DisableFossilSyncOverNATS.
Note on naming: the issue brief proposed Config.ServeFossilSyncOverNATS defaulting to true. Go bool zero-value semantics make default-true awkward without pointers, so the inverted-name shape (DisableX, defaults false = enabled) was used instead.
Closes part of #137 (gap B — bones internal/coord/hub.go).
* feat(hub): FileToCommit.Perm + CommitOpts.Time pass-through (#137 gap C)
Adds two additive fields to the public hub commit surface so consumers no longer need libfossil-direct access to preserve executable bits or seed reproducible commit timestamps:
- hub.FileToCommit.Perm — "x" marks the file executable on the resulting manifest's F-card. Empty preserves prior behavior. Other libfossil perm markers pass through verbatim. - hub.CommitOpts.Time — explicit commit timestamp; zero defaults to time.Now().UTC() at commit time, matching the prior behavior.
Tests cover round-tripping the executable bit via libfossil.Repo.ListFiles, honoring an explicit time via Timeline, and falling back to "now" on the zero value.
Closes part of #137 (gap C — bones internal/hub/hub.go seed path).
* ci: include #137 sim tests in the serve-tests allowlist
Adds TestAgentNew_ and TestHubNATSFossilSync_ to the sim serve-tests regex in both Makefile and test.yml so the new clone-if-absent and hub-NATS-subscriber tests actually run in CI alongside TestHubLeafE2E. (user: [email protected], size: 2757) [annotate] [blame] [check-ins using]
package hub import ( "context" "testing" "time" libfossil "github.com/danmestas/libfossil" _ "github.com/danmestas/libfossil/db/driver/modernc" ) func TestHub_FileToCommit_PermXMarksExecutable(t *testing.T) { h := newTestHub(t) ctx := context.Background() rev, err := h.Commit(ctx, CommitOpts{ Files: []FileToCommit{ {Name: "script.sh", Content: []byte("#!/bin/sh\necho hi\n"), Perm: "x"}, {Name: "data.txt", Content: []byte("hi\n")}, }, Message: "perm round-trip", Author: "hub", }) if err != nil { t.Fatalf("Commit: %v", err) } r := h.Repo().handle rid, err := r.ResolveVersion(string(rev)) if err != nil { t.Fatalf("ResolveVersion(%s): %v", rev, err) } entries, err := r.ListFiles(rid) if err != nil { t.Fatalf("ListFiles: %v", err) } got := map[string]string{} for _, e := range entries { got[e.Name] = e.Perm } if got["script.sh"] != "x" { t.Errorf("script.sh perm = %q, want %q", got["script.sh"], "x") } if got["data.txt"] != "" { t.Errorf("data.txt perm = %q, want empty (default)", got["data.txt"]) } } func TestHub_CommitOpts_TimeIsHonoured(t *testing.T) { h := newTestHub(t) ctx := context.Background() want := time.Date(2024, 6, 15, 12, 30, 0, 0, time.UTC) rev, err := h.Commit(ctx, CommitOpts{ Files: []FileToCommit{{Name: "f.txt", Content: []byte("x")}}, Message: "explicit time", Author: "hub", Time: want, }) if err != nil { t.Fatalf("Commit: %v", err) } r := h.Repo().handle rid, err := r.ResolveVersion(string(rev)) if err != nil { t.Fatalf("ResolveVersion: %v", err) } entries, err := r.Timeline(libfossil.LogOpts{Start: rid, Limit: 1}) if err != nil { t.Fatalf("Timeline: %v", err) } if len(entries) == 0 { t.Fatal("Timeline returned no entries") } got := entries[0].Time.UTC() if !got.Equal(want) { t.Errorf("commit time = %s, want %s", got, want) } } func TestHub_CommitOpts_ZeroTimeDefaultsToNow(t *testing.T) { h := newTestHub(t) ctx := context.Background() before := time.Now().UTC().Add(-1 * time.Second) rev, err := h.Commit(ctx, CommitOpts{ Files: []FileToCommit{{Name: "f.txt", Content: []byte("x")}}, Message: "default time", Author: "hub", }) if err != nil { t.Fatalf("Commit: %v", err) } after := time.Now().UTC().Add(1 * time.Second) r := h.Repo().handle rid, err := r.ResolveVersion(string(rev)) if err != nil { t.Fatalf("ResolveVersion: %v", err) } entries, err := r.Timeline(libfossil.LogOpts{Start: rid, Limit: 1}) if err != nil { t.Fatalf("Timeline: %v", err) } if len(entries) == 0 { t.Fatal("Timeline returned no entries") } got := entries[0].Time.UTC() if got.Before(before) || got.After(after) { t.Errorf("commit time = %s, want in [%s, %s]", got, before, after) } }